算是炒冷饭吧,最近看React源码发现有一些原型与继承方面的东西没看太明白,便计划花两天重温这方面的东西,以便之后有更好的脑回路。
概念
prototype
显式原型对象,每一个函数(除了bind)在创建之后都会拥有一个名为 prototype 的内部属性,它指向函数的原型对象。用来实现基于原型的继承与属性的共享。
__proto__
隐式原型对象,是对象的内部属性, 任意对象都有一个内置属性 [[prototype]]
,在ES5之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过 __proto__
访问并且ES5可以通过 Object.getPrototypeOf(target)
访问。它指向创建这个对象的函数 constructor
的 prototype
, 对象依赖它构成原型链进行向上查询。
只有函数才有显示原型属性 prototype
所有对象都有隐式原型属性 __proto__
包括函数的原型 Function.prototype === Object.__proto__
Object Function
,String
,Number
,Boolean
…(null 不算)所有对象都拥有 __proto__
属性,所以才都具有对象的特点。所有这些原生构造函数的 __proto__
统统指向 Function.prototype
。而它的__proto__
又指向 Object.prototype
。所以才称万物皆对象 。
一个对象的隐式原型指向构造该对象的构造函数的显式原型对象。
var str = String ('seven' )str.__proto__ === String .prototype
无论是通过字面量创建还是构造器亦或者是new+构造器创建,js都会帮我们自动装箱完成实例对象的转换。
Function Function 是一个比较独特的对象,即是对象,也是函数。
var Foo = Function ('a' ,'b' ,'return a+b' )function Foo (a,b ) { return a + b}Foo.__proto__ === Function .prototype var foo = new Foo();foo.__proto__ === Foo.prototype
字面量创建等同于调用构造器创建,但和 new
创建出来的 实例对象 又不同。牵扯到“装箱”、“拆箱”…扯远了…
比如上文中 Foo
也有 __proto__
属性,前面提过,__proto__
指向的是 构造函数的显式原型 ,Foo
由 Function
实例化而来,所以 Foo.__proto__
指向的是 Function.prototype
,不止是Function,Object和其他类型都是一样的道理。
prototype 函数不仅能做对象能做的事情之外,还有个”特权”属性 prototype
。这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法
把上文中的 Foo.prototype
打印出来
{ constructor : ƒ Foo() __proto__: Object }
Foo
的显式原型对象也是对象,原型对象的构造函数都是 Object
,它的 __proto__
属性当然是指向 Object.prototype
。
Foo.prototype.__proto__ === Object .prototype
假如我们想给数组原型添加一个去重排序方法 uniqueFlatWithSort
,让所有数组都可以使用。应该都知道直接往Array.prototype
上加
Array .prototype.uniqueFlatWithSort = function ( ) { return [...new Set (this .flat(Infinity ))].sort((a,b )=> a - b) } var arr = [ [1 , 2 , 2 ], [3 , 4 , 5 , 5 ], [6 , 7 , 8 , 9 , [11 , 12 , [12 , 13 , [14 ] ] ] ], 10 ];arr.uniqueFlatWithSort()
我们都知道属性是通过隐式原型__proto__
递归向上原型链查找的,而隐式原型指向的正是构造函数Array
的显式原型prototype
。
还是上面的例子
var str = String ('seven' )str.padEnd(10 ,"$" ) str.padEnd === str.__proto__.padEnd === String .prototype.padEnd
str
首先进行装箱操作,转化成字符串对象,String {"seven"}
。
首先在 str
上找不到 padEnd
的属性,开始进行原型链向上查找。
便去 str.__proto__
上找,也就是 String.prototype
,发现了String.prototype.padEnd
并返回…
如果字符串 str
想调用 Object
的 hasOwnProperty
方法。
同样首先进行装箱操作,转化成字符串对象,String {"seven"}
。
接着在自身查找有无 hasOwnProperty
方法,发现没有,开始进行原型链向上查找。
str.__proto__
也就是 String.prototype
仍然没有该属性。
再往上去 str.__proto__.__proto__
上找,也就是 String.prototype
原型对象的构造函数 Object
显式原型 prototype
上去找。
找到 Object.prototype.hasOwnProperty
并返回。
这也就是为什么一般都会将实例方法创建前挂载在其显式原型上,好让子类的隐式原型通过进行原型链向上查找。instanceOf 便是这个原理遍历原型链。
调用一个方法的时候,首先在对象本身属性内查找,没有则到 obj.__proto__
隐式原型内查找,如果还没有,就到obj.__proto__.__proto__
…。这条向上查找的链路就被称为原型链。
最终找到Object.prototype
,此时如果仍然没有则返回 undefined
,因为再往上就是终点了。
Object .prototype.__proto__ === null
prototype 还有一个属性 constructor
,它的指针指回构造函数。
foo.__proto__.constructor === Foo
当new一个函数的时候,执行的是原型链中的构造函数.
这张图肯定不会陌生,看完前面的就能明白。
按照先前的总结,Foo.prototype
是一个原型对象 ,它有两个属性:__proto__
和 constructor
,前者已经熟悉
function Foo ( ) {}Foo.prototype.constructor === Foo
这一步得出函数的显式原型的构造函数指向 函数自身。
这个好理解,循环引用
Foo === Foo.prototype.constructor === Foo.prototype.constructor.prototype.constructor
原型的关系
所有构造器(函数)的proto 都指向Function.prototype
Object .__proto__ === Function .prototype; Function .__proto__ === Function .prototype; Number .__proto__ === Function .prototype; Boolean .__proto__ === Function .prototype; String .__proto__ === Function .prototype; Object .__proto__ === Function .prototype; Array .__proto__ === Function .prototype; RegExp .__proto__ === Function .prototype; Error .__proto__ === Function .prototype; Date .__proto__ === Function .prototype;
既然是构造函数,那他就是 Function
的实例,所以 原生构造函数.__proto__ === Function.prototype
。
也就有了
String .__proto__ === Boolean .__proto__RegExp .__proto__ === Error .__proto__Date .__proto__ === Number .__proto__
同理,函数原型的隐式原型都是对象,所以构造函数是 Object
,Function.prototype.__proto__ === Object.prototype
,也就是
Object .__proto__.__proto__ === Object .prototype; Function .__proto__.__proto__ === Object .prototype; Number .__proto__.__proto__ === Object .prototype; Boolean .__proto__.__proto__ === Object .prototype; String .__proto__.__proto__ === Object .prototype; Object .__proto__.__proto__ === Object .prototype; Array .__proto__.__proto__ === Object .prototype; RegExp .__proto__.__proto__ === Object .prototype; Error .__proto__.__proto__ === Object .prototype; Date .__proto__.__proto__ === Object .prototype;
但是话又说回来了,既然所有对象都是通过构造器实例化出来的,但是构造器也是函数!到底是先有 Function
还是先有 Object
?
而且为什么函数的原型对象是个函数typeof Function.prototype === 'function'
?
然后为什么它是函数反而没有 prototype
特权属性: Function.prototype.prototype === undefined
按道理不应该是 Function.__proto__ === Object.prototype
?还是说 Function
其实是通过 Function.prototype
构造器实例化的,Function
本身只是个实例。或者说他们的关系就是一个伪命题
fn.__proto__ = obj.prototype obj.__proto__ = fn.prototype
函数对象到底是什么?
虽然在winter
的重学前端专题中第8节对函数对象的定义是拥有浏览器内建call方法的对象 。但是解释这个JS版的鸡生蛋蛋生鸡的问题仍然有点勉强,或许等以后刨析V8源码才能一探究竟(立下Flag)。关系图如下:
Instanceof Instanceof 通常用来判断一个实例是否属于某种类型。
比如
function Foo ( ) {}var foo = new Foo();console .log(foo instanceof Foo)
又亦如原型继承的多层继承关系。
function Bar ( ) {}function Foo ( ) {}Foo.prototype = new Bar(); var foo = new Foo();console .log(foo instanceof Foo)console .log(foo instanceof Bar)
觉得很简单?
console .log(Object instanceof Object ); console .log(Function instanceof Function ); console .log(Function instanceof Object ); console .log(Object instanceof Function ); console .log(Array instanceof Object ); console .log(Array instanceof Function ); console .log(String instanceof Function ); console .log(String instanceof Object ); console .log(Number instanceof Number ); console .log(String instanceof String ); console .log(Boolean instanceof Boolean ); console .log(Array instanceof Array ); console .log(RegExp instanceof RegExp ); console .log(Symbol instanceof Symbol ); console .log(Error instanceof Error ); console .log(Foo instanceof Function ); console .log(Foo instanceof Foo);
关于 instanceof
运算符的定义,厚着脸皮把人家注释好的粘过来。链接在底部。
11.8 .6 The instanceof operatorThe production RelationalExpression: RelationalExpression instanceof ShiftExpression is evaluated as follows: 1. Evaluate RelationalExpression.2. Call GetValue(Result(1 )).3. Evaluate ShiftExpression.4. Call GetValue(Result(3 )).5. If Result(4 ) is not an object, throw a TypeError exception.6. If Result(4 ) does not have a [[HasInstance]] method, throw a TypeError exception.7. Call the [[HasInstance]] method of Result(4 ) with parameter Result(2 ).8. Return Result(7 ).15.3 .5 .3 [[HasInstance]] (V)Assume F is a Function object. When the [[HasInstance]] method of F is called with value V,the following steps are taken: 1. If V is not an object, return false .2. Call the [[Get]] method of F with property name "prototype" .3. Let O be Result(2 ).4. If O is not an object, throw a TypeError exception.5. Let V be the value of the [[Prototype]] property of V.6. If V is null , return false . 7. If O and V refer to the same object or if they refer to objects joined to each other (section 13.1 .2 ), return true . 8. Go to step 5.
看起来比较难以理解,逻辑最终通过左侧 L.__proto__
隐式原型的向上查找。
function instance_of (L, R ) { var Right = R.prototype; L = L.__proto__; while (true ) { if (L === null ) return false ; if (L === Right) return true ; L = L.__proto__; } }
结合原型的关系 一节,为什么 Function
与 Object
会有这么奇怪的关系就懂了。
创建对象 创建新对象通常有三种方式,new、字面量创建、Object.create()。在日常开发用的最多是字面量创建,但是字面量是为了方便开发人员而设置的语法糖,故只有两种方法。而Object.create是ES5新增的方法。
当你想复用这条原型链的时候,可以用 Object.create()
。
function Bar ( ) {}Bar.prototype.getOwner = function ( ) { return this .name} Bar.prototype.name = 'Floyd' var bar = new Bar()var fiz = Object .create(bar.__proto__)console .log(fiz.__proto__ === bar.__proto__) console .log(fiz.getOwner())
在早期开发,无法通过直接访问原型的方式复用原型链。
Object.create(proto,[propertiesObject])
接受两个参数,proto
是新创建对象的原型对象,propertiesObject
可选属性。要添加到新对象的可枚举的属性描述符以及相应的属性名称,需要注意的是,添加的属性不会被挂载到原型链上去,仅仅作用于本身属性。
比如
var opt = Object .prototypevar o = Object .create(opt,{ foo: { writable:true , configurable:true , value: "hello" } }) o.__proto__ === opt.prototype o.hasOwnProperty('foo' )
看到这其实我们很容易实现一个简单版本的 polyfill.
var isObject = (obj ) => obj && typeof obj === 'object' && !Array .isArray(obj)function myCreate (proto, Properties ) { function F ( ) {} F.prototype = proto; var obj = new F(); if (isObject(Properties)) { Object .defineProperties(obj, Properties); } return obj } var o = myCreate(opt,{ foo: { writable:true , configurable:true , value: "hello" } }) o.__proto__ === opt.prototype o.hasOwnProperty('foo' )
在函数内部创建一个临时性的构造函数,将传入的对象作为这个构造函数的原型,最后返回临时函数的新实例。
为什么要说是简单版本,暂时还不支持传 null
;
在Vue的源码里使用了大量的Object.create(null)
。这么有什么好处?
我们分别打印下Object.create(null)
和 Object.create({})
的结果。
使用create
创建的对象,没有任何属性,显式No properties
,我们可以制定一个很纯净的对象,所有的方法包括toString
、hasOwnProperty
等方法。没有了”包袱”代表使用for in
可以完全避免遍历原型链上的属性,节约了性能损耗,并且也可以当成一个干净的数据字典来使用。
知道了原理,我们就好办多了。
function myCreate (proto, Properties ) { if (proto === null ){ var pureObj = new Object ({}) pureObj.__proto__ = null return pureObj } function F ( ) {} F.prototype = proto; var obj = new F(); if (isObject(Properties)) { Object .defineProperties(obj, Properties); } return obj }
测试用例通过
继承 在JS中,被继承的函数称为超类型(父类,基类也行),继承的函数称为子类型(子类,派生类)。
继承也没想象中那么绕,那么难以理解。只需要记住继承的原则
复用超类的原型对象上的私有属性和方法
属性隔离,实例之间互不影响
明确子类与超类的继承关系
复用原型对象这个都明白,无非是属性与方法的复用;属性隔离是表示相互不影响,a是A的实例,修改了a就不能影响到A;明确继承关系则是:比如a是A的实例,那我就要有办法知道a和A的关系。搞明白这三点,相信你就有了更好的脑回路去理解它。
继承的方式有很多种,外界对此也没有准确的认定到底有多少种方式,褒贬不一,主流通常有7种方式:
原型链继承
借用构造函数继承
组合模式继承
共享原型继承
原型式继承
寄生式继承
寄生式组合继承
(题外)ES6中class 的继承
原型链继承 原型链继承前面例子已经用到多次。
function Foo ( ) { this .name = 'seven' } Foo.prototype.getName = function ( ) { return this .name } var foo = new Foo();foo.getName()
通过实例化一个新的函数,子类的原型指向了父类的实例,子类就可以调用其父类原型对象上的私有属性和公有方法。
原型陷阱 还是上面的例子,当我们尝试调用一个不存在的属性
console .log(foo.getOwner)
原型链上没有这个方法,去修改它的原型
function Bar ( ) {}Bar.prototype.getOwner = function ( ) { return this .name } Foo.prototype = new Bar() console .log(foo.getOwner) console .log(foo.constructor)
都已经替换原型了还是没有更新,表示原型链没有实时性,再测试下新建
var fizz = new Foo()console .log(fizz.getOwner()) console .log(fizz.constructor) console .log(fizz.__proto__)
这时新建的对象可以访问更新后的原型,因为完整替换了 prototype
,构造函数又不对了,本来constructor
属性应该指向Foo
,结果却指向了Bar
(访问了bar.__proto__.constructor
),这就是原型陷阱。完整的替换了原型对象导致访问了新对象的构造函数。
我们只需要重新指定bar
的构造函数即可。
var bar = new Bar()bar.__proto__.constructor = Foo Foo.prototype = bar.__proto__ var fizz = new Foo()console .log(fizz.getOwner()) console .log(fizz.constructor)
现在就恢复正常了,此时原型链为
.__proto__
.__proto__
.__proto__
.__proto__
fizz
fizz.__proto__
fizz.__proto__.__proto__
fizz.__proto__.__proto__.__proto__
Bar.prototype
Bar.prototype.__proto__
Bar.prototype.__proto__.__proto__
Object.prototype
null
实际上最终不会访问到Object.__proto__
,例如foo.freeze === undefined
。
搞明白原型陷阱之后,我们复习一下,把原型继承搞得稍微复杂一些
function Parent (name,age ) { this .name = name; this .age = age; this .skill = ['cook' ,'clean' ,'run' ] this .say = function ( ) { console .log(this .name) } } Parent.prototype.setName = function ( ) {} function Children (name ) { this .children = name; this .speak = function ( ) { console .log(this .childrenName) } } Children.prototype = new Parent('Seven' ,24 ) var c1 = new Children('c1' )var c2 = new Children('c2' )
当调用 c1.skill.push('swimming')
的时候,引用类型的值被共享
如果父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2,违反了第二条:属性隔离,实例之间互不影响
.
原型继承的优点
简单,易实现
父类新增原型方法/原型属性,子类都能访问
原型继承的缺点
无法实现多继承
引用类型的值会被实例共享
子类型还无法给超类型传递参数
借用构造函数(对象冒充) 通过call将超类的this指向子类内部,从而达到隔离的效果。
function Parent (name ) { this .name = name; this .skill = ['cook' ,'clean' ,'run' ] } function Children (name ) { Parent.call(this , name); this .age = 24 } var c1 = new Children('c1' )var c2 = new Children('c2' )c1.skill.push('swimming' ); c1.skill c2.skill c1 instanceof Parent c1 instanceof Children
和借用构造函数类似,原理也是使用子类的this冒充父类的this执行其构造函数,所以把它归纳在一起。
function Parent (name ) { this .name = name; this .skill = ['cook' ] this .getSkill = function ( ) { return this .skill } } function Child (name, age ) { this .c = Parent; this .c(name,age); delete this .c; this .job = '厨师' this .getAge = function ( ) { return this .age; } }
引用类型的问题是解决了,但是缺点也很明显,只能继承超类的属性和方法,而无法复用其原型上的属性和方法。而且实例c1
不是 Parent
超类的子类。而且方法都在构造函数中定义,函数无法达到复用,违反了第一条和第三条原则。
借用构造函数的优点
解决了引用类型的值被实例共享的问题
可以向超类传递参数
可以实现多继承(call若干个超类)
借用构造函数的缺点
不能继承超类原型上的属性和方法
无法实现函数复用,由于call有多个父类实例的副本,性能损耗。
原型链丢失
组合模式继承 看完了前两种方式,有聪明的小伙伴一下子就能想到点什么。原型继承将父类实例作为子类原型实现函数复用,主要针对原型链继承;借用父类构造函数继承父类属性并保留传参,同时针对属性隔离,把两种方式结合起来去其糟粕取其精华,岂不美哉?
这种模式就是组合继承。
function Parent (name ) { this .name = name; this .skill = ['cook' ,'clean' ,'run' ] } Parent.prototype.getName = function ( ) { return this .name } function Children (name ) { Parent.call(this , name); this .age = 24 } Children.prototype = new Parent('seven' ) var c1 = new Children('c1' )var c2 = new Children('c2' )c1.hasOwnProperty('name' ) c1.getName() c1.skill.push('swimming' ) c1.skill c2.skill
看起来似乎没有问题,但是它却调用了2次构造函数,一次在子类构造函数内,另一次是将子类的原型指向父类构造的实例,导致生成了2次name和skill,只不过实例屏蔽了原型上的。虽然达成了目的,却不是我们最想要的。
这个问题将在寄生组合式继承里得到解决。
共享原型继承 这种方式下子类和父类共享一个原型。
function Parent ( ) {}Parent.prototype.skill = ['cook' ] function Children (name, age ) { this .name = name; this .age = age; } Children.prototype = Parent.prototype var c1 = new Children("c1" , 20 )var c2 = new Children("c2" , 24 )c1.skill.push("run" ) c1.skill
共享原型继承的优点 简单
共享原型继承的缺点
只能继承父类原型属性方法,不能继承构造函数属性方法
与原型继承一样,存在引用类型问题
原型式继承 这种继承方式普遍用于基于当前已有对象创建新对象,在ES5之前实现方法:
function object (o ) { function F ( ) {} F.prototype = o return new F() } var obj = { name: 'seven' } var o1 = object(obj)obj.name
看完这段代码,是不是觉得和上文Object.create
的 polyfill 雷同?是的,Object.create
的确ES5为了规范原型式继承。
寄生式继承 寄生则是结合原型式继承和工厂模式,将创建的逻辑进行封装,逻辑上与原型式继承没有什么区别。
function create (o ) { var f = object(o); f.getSkill = function ( ) { return this .skill; }; return f; } var obj = { name: 'seven' , skill: ['cook' ,'clean' ,'run' ] } var c1 = create(obj);c1.name
简单而言,寄生式继承就是不用实例化父类了,直接实例化一个临时副本实现了相同的原型链继承。
寄生式继承的优点 没啥优点
寄生式继承的缺点 原型式继承有的缺点它都有,对此我很疑惑为什么外面装个壳就是另一种继承模式了。
寄生式组合继承 顾名思义,寄生式+组合(原型继承+借用构造函数)式继承。总结了上面的几种方式,相信你已经明白了怎么去实现一个寄生组合式继承。
关于在 Babel loose模式下 inherit 的实现方法:
"use strict" ;function _inheritsLoose (subClass, superClass ) { subClass.prototype = Object .create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }
而在正常模式下
function _setPrototypeOf (o, p ) { _setPrototypeOf = Object .setPrototypeOf || function _setPrototypeOf (o, p ) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _inherits (subClass, superClass ) { if (typeof superClass !== "function" && superClass !== null ) { throw new TypeError ("Super expression must either be null or a function" ); } subClass.prototype = Object .create(superClass && superClass.prototype, { constructor : { value: subClass, writable: true , configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
大同小异,都是子类的原型继承自父类的原型,申明一个用于继承原型的 inheritPrototype
方法,通过这个方法我们能够将子类的原型指向超类的原型,从而避免超类二次实例化。
function object (o ) { function F ( ) {} F.prototype = o return new F() } function inheritPrototype (subType, suberType ) { var prototype = object(suberType.prototype); prototype.constructor = subType; subType.prototype = prototype; } function Parent (name ) { this .name = name; this .skill = ['cook' , 'clean' , 'run' ] } Parent.prototype.getSkill = function ( ) { return this .name } function Children (name ) { Parent.call(this , name); this .age = 24 } inheritPrototype(Children, Parent) var c1 = new Children('c1' )var c2 = new Children('c2' )
可以更简短一些,inheritPrototype
原理上就是 Object.create
的实现。
function Parent (name ) { this .name = name; this .skill = ['cook' , 'clean' , 'run' ] } Parent.prototype.getSkill = function ( ) { return this .getSkill } function Children (name ) { Parent.call(this , name); this .age = 24 } Children.prototype = Object .create(Parent.prototype) Children.prototype.constructor = Children; var c1 = new Children('c1' )var c2 = new Children('c2' )console .log(c1 instanceof Children) console .log(c1 instanceof Parent) console .log(c1.constructor) console .log(Children.prototype.__proto__ === Parent.prototype) console .log(Parent.prototype.__proto__ === Object .prototype) c1.skill.push('swimming' ) c1.getSkill() c2.getSkill()
这也是目前最完美的继承方案,也是觉得它与ES6的class的实现方式最为接近。
寄生式组合继承的优点 堪称完美
寄生式组合继承的缺点 代码多
class继承 ES6中,通过class关键字来定义类,子类可以通过extends继承父类。
class Parent { constructor (name){ this .name = name; this .skill = ['cook' , 'clean' , 'run' ] } getSkill(){ return this .skill } static getCurrent(){ console .log(this ) } } class Children extends Parent { constructor (name){ super (name) } } var c1 = new Children('c1' )var c2 = new Children('c2' )console .log(c1 instanceof Children) console .log(c1 instanceof Parent)
总结
constructor
为构造函数,即使未定义也会自动创建。
在父类构造函数内this定义的都是实例属性和方法,其他方法包括 constructor,getSkill
都是原型方法。
static
关键字定义的静态方法都必须通过类名调用,其this指向调用者而并非实例。
通过 extends
可以继承父类的所有原型属性及 static
类方法,子类 constructor
调用 super
父类构造函数实现实例属性和方法的继承。
最后我们看下通过babel编译后的代码,也不是那么难以理解了。
"use strict" ;function _inheritsLoose (subClass, superClass ) { subClass.prototype = Object .create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } var Parent = function ( ) { function Parent (name ) { this .name = name; this .skill = ['cook' , 'clean' , 'run' ]; } var _proto = Parent.prototype; _proto.getSkill = function getSkill ( ) { return this .skill; }; Parent.getCurrent = function getCurrent ( ) { console .log(this ); }; return Parent; }(); var Children = function (_Parent ) { _inheritsLoose(Children, _Parent); function Children (name ) { return _Parent.call(this , name) || this ; } return Children; }(Parent);
写在后面 可能有的同学会疑问为什么不直接写最好的继承方式,反而把先有缺陷的写在前面,看着反而累。
总不能因为又累又难懂、浪费时间就不学了吧。
文中为了节约时间参考了一些文章的例子,如果有哪的思路不对或者有更好的方式,请留言告诉我,洗耳恭听。
参考资料 JavaScript instanceof 运算符深入剖析