JS中的原型与继承(超详细)

每当我们提起原型链时不免会想到原型对象,对象的原型,还有众多的继承方式。于是prototype[[prototype]]constructor等等难免在头脑中打架。

然而原型其实并不是什么高大上的内容,事实上随处可见。每当我们尝试调用对象方法或者获取对象属性时。js引擎都会检测对象上是否存在这个方法或属性,如果没有会检查他的原型链上是否存在这个方法或属性。直到检查到了结果或者遍历完了整条原型链都没有找到。除了对象,例如数组或函数本质上也都是对象,因此原型在js中其实是一个很基础但重要的内容。

本文尝试用尽量简单但详尽的方式揭开原型与继承的神秘面纱,探访这些无处不在又十分有趣的机制。

原型

尽管可以使用javaascript仿造类,但严格来说,javaascript是没有类的。因此如果一定要用类的思想去理解原型可能会让事情变得复杂。
因为类有点像是一个模版,可以对这个模版进行复制,我们称复制后的内容是模版的实例。而js的原型比起复制关系,更像是关联关系。当实例当中没有查找到内容就去关联的对象上查找。一直递归,直到找到为止。而这种关联就是原型的定义。

原型属性

每一个 Javascript对象(null除外)都和另一个对象相关联。“另一个”对象就是我们熟知的原型,毎一个对象都从原型继承属性。没有原型的对象为数不多, Object. prototype就是其中之一。它不继承任何属性。其他原型对象都是普通对象,普通对象都具有原型。

简单的来说,普通对象都具有一个相关联的对象并从上面继承(委托)属性,这个相关联的对象就是原型。

在这里插入图片描述

如图所示,我们创建了一个空对象,该对象出现的__proto__就是对象的原型属性也就是[[prototype]]。它关联了Object对象原型,因此即使这是一个空对象,你仍可以调用foo.toString方法。因为对象上没有没关系,只要原型有就可以使用。

注意,__proto__并不是一个标准属性,尽管大多数现代浏览器都已经支持应当避免在生产环境使用它。该属性已在ES6中被标准化,现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOfObject.setPrototypeOf/Reflect.setPrototypeOf

原型链

那么原型链又是怎么回事呢?让我们接着往下看
在这里插入图片描述

我们用字面量创建了对象foo,这实际上是Object构造函数创建的。因此foo
的[[prototype]]指向Object的原型属性prototype。

接着我们用foo作为原型创建了bar,显而易见。foo是bar的原型。

因此当我们查找bar对象本身,如果没有则查找它的原型foo。还没有就查找foo的原型Object.prototype。

那么Object.prototype的原型是什么呢?
在这里插入图片描述

可以看到是null。一般来说原型链的顶层是Object.prototype.如果继续向上追溯那么就会得到null

我们常常听说原型链的顶层是Object,其实这一说法并不严谨,事实上顶层是Object.prototype,而非Object本身。
在这里插入图片描述

因此原型链就是利用原型让一个引用类型继承另一个引用类型的属性和方法,并一直向上委托关联的链式结构。简单来说,原型之间相互关联形成的链式结构我们称之为原型链。

正如前文所说,js中并没有类。但是可以模拟类的行为。下文我们将讨论如何实现传统的类中的继承,或者你也可以将其理解为原型的委托。

首先,先来从如何创建一个类看起。

function Cat() {
  this.type = 'cat'
}
const tom = new Cat()
复制代码

Cat的实现其实是个函数。es6中有class关键字,但实际上是语法糖。其大部分表现与function构造类并无太多差异。后文我们会来看看es6类的实现与其继承实现,当中会提到与es5的差异。
在这里插入图片描述

构造函数

在前例中,我们将Cat称为构造函数。使用new调用Cat的过程我们称为实例化。那么构造函数和普通函数有什么区别呢?

答案是没有任何区别。所谓构造函数和普通函数并没有任何区。实际上,是new 会劫持所有普通函数并用构造对象的形式来调用它。

那构造函数就是类吗?答案是否定的。构造函数是类的公共标示,原型才是类的唯一标示。
是不是听起来有点绕?这就是说尽管不是同一个构造函数,只要原型相同我们就认为是一类。接下来我们来看个例子来更加深入的了解构造函数。

function Cat() {
  this.type = 'cat'
}
Cat.prototype.eat = function () {
  console.log('eat fish')
}

function Mouse() {
  this.type = 'mouse'
}
Cat.prototype.eat = function () {
  console.log('eat rice')
}

const tom = new Cat()
const jerry = new Mouse()
复制代码
原型对象

与刚刚的那个简单例子相比,这个例子多出了prototype。这个是构造函数的原型对象。还记得刚刚说到每一个对象有一个与之相关的对象,这就是原型属性,也就是对象的原型[[prototype]]。这个prototype又是什么?

每一个函数都包含一个 prototype.属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”( prototype object)。每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。

好家伙,又是对象的原型又是原型对象。这放一块不是更乱了?先别急,我们一起来看看,彻底弄清这两个概念。

先来说对象的原型[[prototype]] (隐藏属性可用非标准属性__proto__获取,注意事项详见上文原型属性)。对象的原型严格来说是对象的原型属性指向的是对象所继承的原型链的上一级。 表达的是继承关系。

函数的原型对象,描述的是与函数本身相关联的对象,仅有函数才prototype对象。指向的是原型链的同级。 你可以理解为对象自身可作为自身的原型,而函数需要借助prototype来描述自身的原型。起到的是补充描述作用。

如果还是很难理解,那来看以下关系图:

在这里插入图片描述

可以看到用链式结构描述的时候,prototype描述的是原型链的同层,[[prototype]]描述的是原型链的上一层。函数也是对象,因此函数也拥有[[prototype]]。我们放在一起对比一下就更好理解了。
在这里插入图片描述

我们将目光聚焦于Cat函数。作为Fucntion的实例,Cat的__proto__指向Function的原型。即Function.prototype。图中绿线标注。

prototype指向自身的原型对象,图中蓝线标注。

想必细心的你已经注意到了,图中的链式结构连接的都是构造函数的原型,而非构造函数本身。这就与类的定义有关类。我们对上面的例子稍加改造,来方便我们理解

类和类型
function Cat() {
  this.type = 'cat'
}
Cat.prototype.eat = function () {
  console.log('eat fish')
}

function Mouse() {
  this.type = 'mouse'
}
Mouse.prototype = Cat.prototype

const tom = new Cat()
const jerry = new Mouse()

Mouse.prototype = {
  eat() {
    console.log('eat rice')
  }
}
复制代码

jerry是由Mouse构造函数初始化的,但是他的原型与Cat相同。那么这个jerry是Mouse类还是Cat类呢?是耗子是猫我们拉出来溜溜
在这里插入图片描述

可以看到尽管由jerry是由Mouse初始化的,但实际上属于Cat”类“。是否会感到有些吃惊。这其实与类的定义有关。当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例。 这与他们是同一个构造函数初始化的无关。

因此,instanceof操作符判断的实际上是构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

这么看来似乎构造函数并没有什么用。但其实并不是,虽然不像原型那样基础,但是构造函数是类的外在表现。当我们为类起名的时候使用的就是构造函数的名称。例如对象的原型链中存在Cat构造函数的原型,我们一般认为这个对象是Cat类,无论其是否由Cat构造函数初始化。

关于构造函数的名称,尽管不论你写大写还是小写对js引擎都没什么区别,但是对于便于理解的角度而言可以快速看出开发者的意图,来区分构造函数与普通函数。因此构造函数的首字母大写可以算是一个公约。

继承

那么我们如何实现原型的继承呢?还是刚刚Cat的例子。我们想创建一个白猫的类,让他继承Cat类,该如何实现呢?

原型链继承

首先我们先思考,当我们实现继承我们实际想实现的是什么?我们是希望我们可以在无需定义的情况下直接使用“父类”已经存在的内容。那么我们刚刚说的原型链就可以实现!只要让WhiteCat的原型指向Cat实例,那么Cat中所有的内容WhiteCat就都会存在了。

function Cat() {
  this.type = 'cat'
}

Cat.prototype = {
  eat () {
    console.log('eat fish')
  },
  favourite: ['eat', 'roll']
}

function WhiteCat() {
  this.color = 'white'
}

WhiteCat.prototype = new Cat()

const tom = new WhiteCat()
复制代码

可以看到Cat实例拥有的方法和属性,WhiteCat也拥有了Cat中的属性和方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S3yUy67f-1621163159814)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p99)]

但这种方法有个严重的副作用

function Cat() {
  this.type = 'cat',
  this.favourite = ['eat', 'roll']
}

Cat.prototype = {
  eat () {
    console.log('eat fish')
  } 
}

function WhiteCat() {
  this.color = 'white'
}

WhiteCat.prototype = new Cat()

const tom = new WhiteCat()
const lisa = new WhiteCat()
tom.favourite.push('run')
console.log(tom.favourite, lisa.favourite)
复制代码

如果原型中存在引用类型,对实例的的操作可能会影响到另一实例。
在这里插入图片描述

另一个问题就是不支持参数。

function Cat(name) {
  this.type = 'cat',
  this.favourite = ['eat', 'roll']
  this.name = name
}

Cat.prototype = {
  eat () {
    console.log('eat fish')
  } 
}

function WhiteCat() {
  this.color = 'white'
}

// 这里不知道实例化时的参数,所以没法支持参数
WhiteCat.prototype = new Cat()

const tom = new WhiteCat()
const lisa = new WhiteCat()
tom.favourite.push('run')
console.log(tom.favourite, lisa.favourite)
复制代码

借用构造函数

除了使用原型链的方式实现继承,我们还可以直接在子类里调用父类的方法。这样也能获得父类中的属性。

function Cat(name) {
  this.type = 'cat'
  this.favourite = ['eat', 'roll']
  this.name = name
}

Cat.prototype = {
  eat () {
    console.log('eat fish')
  } 
}

function WhiteCat(name) {
  this.color = 'white'
  // 调用Cat构造函数
  Cat.call(this, name)
}

// WhiteCat.prototype = new Cat()

const tom = new WhiteCat('tom')
const lisa = new WhiteCat('lisa')
tom.favourite.push('run')
console.log(tom.favourite, lisa.favourite)
复制代码

可以看到刚刚原型链实现的继承中遇到的问题都得到了解决。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZCxk2rsh-1621163159818)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p101)]

但这种方案并不完美,因为父类Cat原型中的方法eat并未出现在实例tom里。那么如果将这两种方案结合是不是就能解决问题了? 让我们来试试

组合继承

function Cat(name) {
  this.type = 'cat'
  this.favourite = ['eat', 'roll']
  this.name = name
}

Cat.prototype = {
  eat () {
    console.log('eat fish')
  } 
}

function WhiteCat(name) {
  this.color = 'white'
  // 调用Cat构造函数,借用构造函数
  Cat.call(this, name)
}

// 原型指向父类实例,形成原型链
WhiteCat.prototype = new Cat()

const tom = new WhiteCat('tom')
const lisa = new WhiteCat('lisa')
tom.favourite.push('run')
console.log(tom.favourite, lisa.favourite)
复制代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ixkHhLk-1621163159821)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p102)]

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 Javascript中最常用的继承模式。

原型式继承

通过以上的例子我们不难看出js的继承不同于传统类的“复制”,而是一种关联。也就是说我们将一个对象关联在对象的[[prototype]]属性上时就实现了对这个对象的继承。基于此:

function relative(object) {
  function f() { }
  // 将对象作为原型对象通过构造函数添加到目标对象的原型属性中
  f.prototype = object
  return new f()
}

person = {
  run () {}
}

const xiaoming = relative(person)
复制代码

可以看到xiaominng完成了对person的继承。构造函数f在此仅仅只是个工具函数,所以不需要专门定义出来,封装在relative即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpASo2gl-1621163159823)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p103)]

有没有更简单的写法? 有!

person = {
  run () {}
}

const xiaoming = Object.create(person)
复制代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bKWWE1LH-1621163159824)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p104)]

ES5中标准化了这种写法,也就是Object.create。你甚至可以使用Object。create完成原型关联的同时也增加对象中的属性
在这里插入图片描述

但是这种方式实现的继承与原型链继承一样存在原型引用类型引用共享问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mQp27LmA-1621163159825)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p106)]

寄生式继承

刚刚的原型继承可以实现对对象的关联,但是无法进一步封装。如果要在刚刚的基础上給进一步加上自己的方法在返回

person = {
  run () {}
}

function Student() {
  const clone = Object.create(person)
  clone.task = function () { console.log('go to school') }
  return clone
}

const xiaoming = new Student()

复制代码

可以看到小明现在既会跑,也要上学了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBzz6EnO-1621163159826)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p108)]

Student其实并没有做原型的关联操作,而是利用原型继承创建对象并在对象中添加一些属性就直接返回。就像是宿主并不直接操作,而由寄生在其中的原型对象操作那样。这种方式叫做寄生式继承。

寄生式组合继承

还记得刚刚的组合式继承吗? 尽管他结合了原型链继承与借用构造继承的优点。但是仍然有一些缺憾。来看刚刚的例子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fwWiKooB-1621163159827)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p109)]

由于实例和原型两次对于Cat的调用,导致重复属性出现在实例和原型上。
因此,如果我们将父类的属性在构造函数返回,而直接将父类原型对象作为子类原型对象的原型即可避免这种重复。让我们对上面的例子稍加改造:

function Cat(name) {
  this.type = 'cat'
  this.favourite = ['eat', 'roll']
  this.name = name
}

Cat.prototype = {
  eat () {
    console.log('eat fish')
  } 
}

function WhiteCat(name) {
  this.color = 'white'
  // 调用Cat构造函数,借用构造函数
  Cat.call(this, name)
}

// 使得WhiteCat的原型对象以Cat的原型对象为原型
WhiteCat.prototype = Object.create(Cat.prototype)
// 原型指向父类实例,形成原型链
// WhiteCat.prototype = new Cat()

const tom = new WhiteCat('tom')
复制代码

此时就可以避免Cat两个调用造成的重复问题了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bz044Kl3-1621163159827)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p110)]

此时WhiteCat的原型对象是以Cat原型对象为原型的空对象。因此我们需要手动对constructor进行赋值。然后利用我们刚刚说到的寄生的方法进行封装

function Cat(name) {
  this.type = 'cat'
  this.favourite = ['eat', 'roll']
  this.name = name
}

Cat.prototype = {
  eat () {
    console.log('eat fish')
  } 
}

function WhiteCat(name) {
  this.color = 'white'
  // 调用Cat构造函数,借用构造函数
  Cat.call(this, name)
}

function inherit(subObj, superObj) {
  subObj.prototype = Object.create(superObj.prototype)
  subObj.prototype.constructor = subObj
}

inherit(WhiteCat, Cat)
// 使得WhiteCat的原型对象以Cat的原型对象为原型
// 原型指向父类实例,形成原型链
// WhiteCat.prototype = new Cat()

const tom = new WhiteCat('tom')
复制代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qnnQRXUP-1621163159828)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p111)]

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

ES6中的Class

ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到。类的数据类型就是函数,类本身就指向构造函数。继续回到我们之前的例子,我们使用Class语法去书写jerry与tom的故事。我们会将下例,使用Class语法改写

function Cat() {
  this.type = 'cat'
}
Cat.prototype.eat = function () {
  console.log('eat fish')
}

function Mouse() {
  this.type = 'mouse'
}
Mouse.prototype.eat = function () {
  console.log('eat rice')
}

const tom = new Cat()
const jerry = new Mouse()
复制代码

Class语法:

class Cat1 {
  constructor() {
    this.type = 'cat'
  }
  eat () {
    console.log('eat fish')
  }
}

class Mouse1 {
  constructor() {
    this.type = 'mouse'
  }
  eat () {
    console.log('eat rice')
  }
}

const tom1 = new Cat1()
const jerry1 = new Mouse1()
复制代码

可以看到这两种写法是等效的。
简单的来说,就是将构造函数放进constructor属性,而将prototype放进class类里。其实这种语法糖能够帮助初学者更加准确的理解类和继承。这样更容易理解原型对象是类的唯一标示,而构造函数是类的初始化函数。
在这里插入图片描述

constructor方法

一个类必须要有constructor方法,如果没有显示的指明constructor,那将会默认添加一个空的。constructor默认返回实例this,但可以显示的指定返回其他对象。

class表达式

与函数相同,class可以采用表达式形式定义

const Cat = class Tom{
  constructor() {
    this.type = 'cat'
  }
  eat () {
    console.log('eat fish')
  }
}
const bob = new Cat()
const lily = new Tom()
复制代码

但是要注意,此时的类名是变量Cat,而非Tom。使用Tom初始化是会报错的。
在这里插入图片描述

如果只是在类的内部使用就是可以的。因此如果不需要在内部使用class后name可省略。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7nTj1OP-1621163159830)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p115)]

与构造函数写法的区别

不存在变量提升

与构造函数不同,class不存在变量提升。因此需要先声明,在调用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a5NzFI8U-1621163159831)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p116)]

可枚举性

与ES5不同,类的内部所有定义的方法,都是不可枚举的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UhXVAO1E-1621163159832)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p117)]

原生构造函数继承

之前无法对内置的构造函数,如Array、Function等进行继承。是因为子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nWwCAYnO-1621163159833)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p122)]

ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this ,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooee39Da-1621163159834)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p123)]

继承

来看一个class继承的例子

class Cat {
  constructor() {
    this.type = 'cat'
  }
  eat () {
    console.log('eat fish')
  }
}

class WhiteCat extends Cat {
  constructor() {
    super()
    this.color = 'white'
  }
  run() {
    console.log('run fast!')
  }
}
const lisa = new WhiteCat()
复制代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOf4G3uA-1621163173258)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p120)]

super关键字

其中的super关键字是首次出现的。super有俩种用法:

  • 作为函数调用时(即 super(...args) ), super 代表父类的构造函数。
  • 作为对象调用时(即 super.propsuper.method() ), super 代表父类。

因为对象总是继承其他对象的,所以可以在任何对象中使用super关键字:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ePgsDcCB-1621163173260)(evernotecid://5C9EC4C2-3173-4E27-96E0-473FCB420905/appyinxiangcom/9154414/ENResource/p121)]

上例所示就是,在Object获取其原型构造函数的名字。

那如果在class中不调用super(),只进行this赋值会怎样?
在这里插入图片描述

会报错!这是因为子类没有自己的this对象,而是继承父类this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。这点与ES5不同。
下面是babel转化为ES5的部分代码:

var Cat = /*#__PURE__*/function () {
  "use strict";

  function Cat() {
    _classCallCheck(this, Cat);

    this.type = 'cat';
  }

  _createClass(Cat, [{
    key: "eat",
    value: function eat() {
      console.log('eat fish');
    }
  }]);

  return Cat;
}();

var WhiteCat = /*#__PURE__*/function (_Cat) {
  "use strict";

  _inherits(WhiteCat, _Cat);
  
  // 进行原型关联,返回了实例this对象
  var _super = _createSuper(WhiteCat);

  function WhiteCat() {
    var _this;

    _classCallCheck(this, WhiteCat);

    _this = _super.call(this);
    // this 关联原型未挂载实例属性
    // _this为实例本身
    console.log(_super, _super.run, _this, this, 'instance')
    _this.color = 'white';
    return _this;
  }

  _createClass(WhiteCat, [{
    key: "run",
    value: function run() {
      console.log('run fast!');
    }
  }]);

  return WhiteCat;
}(Cat);

var lisa = new WhiteCat();

function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  // 此处使用apply调用,this指向实例
  return function _createSuperInternal() {
    // Super为Cat构造函数
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      // 获取this原型对象的构造器
      // 因为在继承中将constructor指向了子类构造器,所以获取的是子类构造器,也就是WhiteCat构造函数
      var NewTarget = _getPrototypeOf(this).constructor;
      // 以父类为构造函数,调用该构造函数。并将新对象的constructor指向NewTarget,等同于
      // superProto = Object.create(Super.prototype);
      // NewTarget.apply(superProto, arguments);
      // 返回子类构造函数创建的实例
      result = Reflect.construct(Super, arguments, NewTarget);
      console.log(result, typeof result, 'result')
      console.log(Super, typeof Super, 'Super')
      console.log(NewTarget, typeof NewTarget, 'NewTarget')
    } else {
      // super函数调用结果
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}
复制代码

可以看到基本上是对寄生式组合继承的一个改造。ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到 this上面
ES6则不同,实质是先创造父类的实例对象this(所以必须先调用 super方法),然后再用子类的构造函数修改this

后记

关于js中的原型关联机制,在一般情况下将会按照类的思想将其命名为继承。虽然js中没有类的概念但仍可以使用js模拟出类的行为。而在《你不知道的jaavaascript(上卷)》当中,则对这种做法嗤之以鼻。并详细的介绍了类思想和委托思想的区别,并认为应当从委托而非类与继承的角度去理解原型。
但根据笔者的学习经历则认为无论是红宝书和犀牛书中的类与继承思想还是你不知道的js中的委托行为,只要作为学习者可以自洽的理解整套的逻辑体系都是没有问题的,毕竟不管黑猫白猫能抓到耗子的都是好猫。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享