你真的理解JS的继承了吗?

噫吁嚱,js之难,难于上青天javascript

本文小纲介绍

请先看下图,若是各位大佬以为soeasy,请直接 插队这里 查看class继承 。java

js 的继承是经过原型链实现的,因此想要理解 js 的继承,必须先吃透原型链! ios

原型和原型链

js 中的几乎全部对象都有一个特殊的[[Prototype]]内置属性,用来指定对象的原型对象,这个属性实质上是对其余对象的引用。在浏览器中通常都会暴露一个私有属性 __proto__,其实就是[[Prototype]]的浏览器实现。假若有一个对象var obj = {},那么能够经过obj.__proto__ 访问到其原型对象Object.prototype,即obj.__proto__ === Object.prototype。对象有[[Prototype]]指向一个原型对象,原型对象自己也是对象也有本身的[[Prototype]]指向别的原型对象,这样串接起来,就组成了原型链。es6

var obj = [1, 2, 3]
obj.__proto__ === Array.prototype // true
Number.prototype.__proto__ === Object.prototype // true
Array.prototype.__proto__ === null // true
obj.toString()
复制代码

能够看出,上例中存在一个从objnull的原型链,以下:数组

obj----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null
复制代码

上例中最后一行调用obj.toString()方法的时候,js 引擎就是沿着这条原型链查找toString方法的。js 首先在obj对象自身上查找toString方法;未找到,继续沿着原型链查找Array.prototype上有没有toString;未找到,继续沿着原型链在Object.prototype上查找。最终在Object.prototype上找到了toString方法,因而泪流满面的调用该方法。这就是原型链最基本的做用。浏览器

上面我说“js 中的几乎全部对象都有一个特殊的[[Prototype]]内置属性”,为何不是所有呢?由于 js 能够建立没有内置属性[[Prototype]]的对象:闭包

var o = Object.create(null)
o.__proto__ // undefined
复制代码

Object.create是 es5 的方法,全部浏览器都已支持。该方法建立并返回一个新对象,并将新对象的原型对象赋值为第一个参数。在上例中,Object.create(null)建立了一个新对象并将对象的原型对象赋值为null。此时对象 o 是没有内置属性[[Prototype]]的(不知道为何o.__proto__不是null,但愿知道的大佬评论解释下,万分感激)。函数

es5寄生组合继承

es5 的继承是经过修改子类的原型对象来实现的:ui

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'
复制代码
  1. 首先,这段代码声明了父类 SuperType
  2. 其次,声明了父类的原型对象方法 sayName
  3. 再次,声明了子类 SubType,并在将来将要新建立的 SubType 实例环境上调用父类 SuperType.call,以获取父类中的 namecolors 属性。
  4. 再次,用 Object.create() 方法把子类的原型对象上的 __proto__ 属性指向了父类的原型对象,并把子类构造函数从新赋值为子类。
  5. 而后,给子类的原型对象上添加方法 sayAge。
  6. 最后初始化实例对象instance。(调用new SubType('gim', '17')的时候会生成一个__proto__指向SubType.prototype的空对象,而后把this指向这个空对象。在添加完name、colors、age属性以后,返回这个‘空对象’,也就是说instance最终就是这个‘空对象’。)

此时,代码中生成的原型链关系以下图所示(下面三张图撸了一下午,喜欢的帮忙点个赞谢谢):this

es5寄生组合继承

  • 子类的原型对象的 __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)对象。

  • 在 es5 的实现中,子类构造函数的 __proto__ 直接指向的是 Function.prototype 黑色的带箭头的线则是 es5 继承中产生的‘反作用’,使得全部的函数的 __proto__ 指向了 Function.prototype,并最终指向 Object.prototype,从而使得咱们声明的函数能够直接调用 toString(定义在Function.prototype上)、hasOwnProperty(定义在Object.prototype上) 等方法,如:SubType.toString()、SubType.hasOwnProperty()等。

    下面看看es6中有哪些不一样吧。

es6的继承

es6 的继承是由 class ... extends ... 实现的:

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)

虽然结果可能如你所料的实现了原型链继承,可是这里仍是有个须要注意的点值得一说。

es6的class ... extends ...继承

如图,es6中的 class 继承存在两条继承链:

  1. 子类的原型对象的__proto__属性,表示方法的继承,老是指向父类的prototype属性。 这点倒和经典继承是一致的。 如红线所示,子类SubTypeprototype属性的__proto__指向父类SuperTypeprototype属性。 至关于调用Object.setPrototypeOf(SubType.prototype, SuperType.prototype); 由于和经典继承相同,这里再也不累述。

  2. 子类构造函数的__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可以在发生加减乘除的强制类型转换的时候,数组内的每项会自动执行加性运算。

以上就是js实现继承的两种模式,能够发现class继承和es5寄生组合继承有类似之处,也有不一样的地方。虽然class继承存在一些问题(如暂不支持静态属性等),可是子类的内置原型指向父类这点是个不错的改变,这样咱们就能够利用原生构造函数(Array等)构建本身想要的类了。

kyle大佬提到的行为委托

在读《你不知道的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()方法,都 es10 了还未实现 es6 规定的方法,真是够了。(P.S.终于在9.13号ios实现了 setPrototypeOf 方法,真的是 f 了)

kyle大佬倡导的行为委托

小子愚钝,若是行为委托彻底可以实现实现class继承的功能,并且更加简单和清晰,咱们开发的过程当中为何不愉快的尝试用一下呢?

相关文章
相关标签/搜索