噫吁嚱,js之难,难于上青天
学习js的这几年,在原型链和继承上花了不知道多少时间,每当自觉得已经吃透它的时候,老是不经意的会出现各类难以理解的幺蛾子。也许就像kyle大佬说的那样,js的继承真的是‘蠢弟弟’设计模式吧。javascript
阅读本文以前先约定,本文中称 __proto__ 为 内置原型,称 prototype 为 原型对象,构造函数 SubType 和 SuperType 分别称为子类和父类。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } SubType.prototype = Object.create(SuperType.prototype, { constructor: { value: SubType, enumerable: false, writable: true, configurable: true } }) SubType.prototype.sayAge = function(){ alert(this.age); }; let instance = new SubType('gim', '17'); instance.sayName(); // 'gim' instance.sayAge(); // '17'
SuperType
sayName
。SubType
,并在将来将要新建立的 SubType
实例环境上调用父类 SuperType.call
,以获取父类中的 name
和 colors
属性。Object.create()
方法把子类的原型对象上的内置对象 __proto__
指向了父类的原型对象,并把子类构造函数从新赋值为子类。instance
。(调用new SubType('gim', '17')
的时候会生成一个__proto__
指向SubType.prototype
的空对象,而后把this
指向这个空对象。在添加完name、colors、age
属性以后,返回这个‘空对象’,也就是说instance
最终就是这个‘空对象’。)此时,代码中生成的原型链关系以下图所示(下面三张图撸了一下午,喜欢的帮忙点个赞谢谢):java
__proto__
指向父类的原型对象。 图中有两种颜色的带箭头的线,红色的线是咱们生成的实例的原型链,是咱们之因此能调用到 instance.sayName()
和 instance.sayAge()
的根本所在。当调用instance.sayName()
的时候,js引擎会先查找instance
对象中的自有属性。未找到sayName
属性,则继续沿原型链查找,此时instance
经过内置原型__proto__
链到了SubType.prototype
对象上。但在SubType.prototype
上也未找到sayName
属性,继续沿原型链查找,此时SubType.prototype
的__proto__
链到了SuperType.prototype
对象上。在对象上找到了sayName
属性,因而查找结束,开始调用。所以调用instance.sayName()
至关于调用了instance.__proto__.__proto__.sayName()
,只不过前者中sayName
函数内this
指向instance
实例对象,然后者sayName
函数内的this
指向了SuperType.prototype(instance.__proto__.__proto__ === SuperType.prototype)
对象。__proto__
直接指向的是 Function.prototype
。 黑色的带箭头的线则是 es5 继承中产生的‘反作用’,使得全部的函数的 __proto__
指向了 Function.prototype
,并最终指向 Object.prototype,从而使得咱们声明的函数能够直接调用 toString
(定义在Function.prototype上)、hasOwnProperty
(定义在Object.prototype上) 等方法,如:SubType.toString()、SubType.hasOwnProperty()
等。下面看看es6中有哪些不一样吧。es6
class SuperType { constructor(name) { this.name = name this.colors = ["red", "blue", "green"]; } sayName() { alert(this.name) } } class SubType extends SuperType { constructor(name, age){ super(name) this.age = age } sayAge() { alert(this.age) } } let instance = new SubType('gim', '17'); instance.sayName(); // 'gim' instance.sayAge(); // '17'
能够明显的发现这段代码比以前的更加简短和美观。es6 class
实现继承的核心在于使用关键字 extends
代表继承自哪一个父类,而且在子类构造函数中必须调用 super
关键字,super(name)
至关于es5继承实现中的 SuperType.call(this, name)
。设计模式
虽然结果可能如你所料的实现了原型链继承,可是这里仍是有个须要注意的点值得一说。数组
class
继承存在两条继承链:prototype
属性的__proto__
属性,表示方法的继承,老是指向父类的prototype
属性。这点倒和经典继承是一致的。
如红线所示,子类SubType
的prototype
属性的__proto__
指向父类SuperType
的prototype
属性。
至关于调用Object.setPrototypeOf(SubType.prototype, SuperType.prototype)
;
由于和经典继承相同,这里再也不累述。闭包
__proto__
属性,表示构造函数的继承,老是指向父类。这是个值得注意的点,和es5中的继承不一样,如蓝线所示,子类SubType
的__proto__
指向父类SuperType
。至关于调用了Object.setPrototypeOf(SubType, SuperType)
;
es5继承中子类和父类的内置原型直接指向的都是Function.prototype
,因此说Function
是全部函数的爸爸。而在es6class...extends...
实现的继承中,子类的内置原型直接指向的是父类。
之因此注意到这点,是由于看 kyle 大佬的《你不知道的javascript 下》的时候,看到了class MyArray extends Array{}
和var arr = MyArray.of(3)
这两行代码,很不理解为何MyArray
上面为何能调到of
方法。由于按照es5中继承的经验,MyArray.__proto__
应该指向了Function.prototype
,然后者并无of方法。当时感受世界观都崩塌了,为何我之前的认知失效了?次日重翻阮一峰老师的《ECMAScript6入门》才发现原来class
实现的继承是不一样的。函数
知道了这点,就能够根据需求灵活运用Array
类构造本身想要的类了:学习
class MyArray extends Array { [Symbol.toPrimitive](hint){ if(hint === 'default' || hint === 'number'){ return this.reduce((prev,curr)=> prev+curr, 0) }else{ return this.toString() } } } let arr = MyArray.of(2,3,4); arr+''; // '9'
元属性Symbol.toPrimitive
定义了MyArray
的实例发生强制类型转换的时候应该执行的方法,hint
的值多是default/number/string
中的一种。如今,实例arr
可以在发生加减乘除的强制类型转换的时候,数组内的每项会自动执行加性运算。this
以上就是js实现继承的两种模式,能够发现class继承和es5寄生组合继承有类似之处,也有不一样的地方。虽然class继承存在一些问题(如暂不支持静态属性等),可是子类的内置原型指向父类这点是个不错的改变,这样咱们就能够利用原生构造函数(Array等)构建本身想要的类了。es5
在读《你不知道的javascript 上》的时候,感触颇多。这本书真的是本良心书籍,让我学会了LHS/RHS,读懂了闭包,了解了词法做用域,完全理解了this指向,基本懂了js的原型链继承。因此当时就忍不住又从头读了一遍。若是说诸多感觉中最大的感觉是啥,那必定是行为委托了。我第一次见过有大佬可以如此强悍(至少没见过国内的大佬这么牛叉的),强悍到直接号召读者抵制js的继承模式(不管寄生组合继承仍是class继承),而且提倡使用行为委托模式实现对象的关联。我真的被折服了,要知道class但是w3c委员会制定出的标准,而且已经普遍的应用到了业界中。关键的关键是,我确实认为行为委托确实更加清晰简单(若有异议请指教)。
let SuperType = { initSuper(name) { this.name = name this.color = [1,2,3] }, sayName() { alert(this.name) } } let SubType = { initSub(age) { this.age = age }, sayAge() { alert(this.age) } } Object.setPrototypeOf(SubType,SuperType) SubType.initSub('17') SubType.initSuper('gim') SubType.sayAge() // 'gim' SubType.sayName() // '17'
这就是模仿上面js继承的两个例子,利用行为委托实现的对象关联。行为委托的实现很是超级极其的简单,就是把父对象关联到子对象的内置原型上,这样就能够在子对象上直接调用父对象上的方法。行为委托生成的原型链没有class继承生成的原型链的复杂关系,一目了然。固然class有其存在的道理,可是在些许场景下,应该是行为委托更加合适吧。但愿safari尽快实现Object.setPrototypeOf()
方法,太out了连ie都支持了。
小子愚钝,若是行为委托彻底可以实现实现class继承的功能,并且更加简单和清晰,咱们开发的过程当中为何不愉快的尝试用一下呢?