ES6 Class 继承与 super

原文 https://javascript.info/class...javascript

Class 继承与 super

class 能够 extends 自另外一个 class。这是一个不错的语法,技术上基于原型继承。java

要继承一个对象,须要在 {..} 前指定 extends 和父对象。git

这个 Rabbit 继承自 Animalgithub

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}


// Inherit from Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}


let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

如你所见,如你所想,extend 关键字其实是在 Rabbit.prototype 添加 [Prototype]],引用到 Animal.prototype编程

因此如今 rabbit 既能够访问它本身的方法,也能够访问 Animal 的方法。数组

extends 后可跟表达式

Class 语法的 `extends' 后接的不限于指定一个类,更能够是表达式。安全

例如一个生成父类的函数:ide

function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}


class User extends f("Hello") {}


new User().sayHi(); // Hello

例子中,class User 继承了 f('Hello')返回的结果。函数

对于高级编程模式,当咱们使用的类是根据许多条件使用函数来生成时,这就颇有用。ui

重写一个方法

如今让咱们进入下一步,重写一个方法。到目前为止,RabbitAnimal 继承了 stop 方法,this.speed = 0

若是咱们在 Rabbit 中指定了本身的 stop,那么会被优先使用:

class Rabbit extends Animal {
  stop() {
    // ...this will be used for rabbit.stop()
  }
}

......但一般咱们不想彻底替代父方法,而是在父方法的基础上调整或扩展其功能。咱们进行一些操做,让它以前/以后或在过程当中调用父方法。

Class 为此提供 super关键字。

  • 使用 super.method(...) 调用父方法。
  • 使用 super(...) 调用父构造函数(仅在 constructor 函数中)。

例如,让兔子在 stop 时自动隐藏:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }


  stop() {
    super.stop(); // call parent stop
    this.hide(); // and then hide
  }

}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stopped. White rabbit hides!

如今,Rabbitstop 方法经过 super.stop() 调用父类的方法。

箭头函数无 super

正如在 arrow-functions 一章中提到,箭头函数没有 super

它会从外部函数中获取 super。例如:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // call parent stop after 1sec
  }
}

箭头函数中的 superstop() 中的相同,因此它按预期工做。若是咱们在这里用普通函数,便会报错:

// Unexpected super
setTimeout(function() { super.stop() }, 1000);

重写构造函数

对于构造函数来讲,这有点棘手 tricky。

直到如今,Rabbit 都没有本身的 constructor
Till now, Rabbit did not have its own constructor.

根据规范,若是一个类扩展了另外一个类而且没有 constructor ,那么会自动生成以下 constructor

class Rabbit extends Animal {
  // generated for extending classes without own constructors

  constructor(...args) {
    super(...args);
  }

}

咱们能够看到,它调用了父 constructor 传递全部参数。若是咱们不本身写构造函数,就会发生这种状况。

如今咱们将一个自定义构造函数添加到 Rabbit 中。除了name,咱们还会设置 earLength

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {


  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }


  // ...
}


// Doesn't work!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.

哎呦出错了!如今咱们不能生成兔子了,为何呢?

简单来讲:继承类中的构造函数必须调用 super(...),(!)而且在使用 this 以前执行它。

...但为何?这是什么状况?嗯...这个要求看起来确实奇怪。

如今咱们探讨细节,让你真正理解其中原因 ——

在JavaScript中,继承了其余类的构造函数比较特殊。在继承类中,相应的构造函数被标记为特殊的内部属性 [[ConstructorKind]]:“derived”

区别在于:

  • 当一个普通的构造函数运行时,它会建立一个空对象做为 this,而后继续运行。
  • 可是当派生的构造函数运行时,与上面说的不一样,它期望父构造函数来完成这项工做。

因此若是咱们正在构造咱们本身的构造函数,那么咱们必须调用 super,不然具备 this 的对象将不被建立,并报错。

对于 Rabbit 来讲,咱们须要在使用 this 以前调用 super(),以下所示:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {

    super(name);

    this.earLength = earLength;
  }

  // ...
}


// now fine
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10

Super 的实现与 [[HomeObject]]

让咱们再深刻理解 super 的底层实现,咱们会看到一些有趣的事情。

首先要说的是,以咱们迄今为止学到的知识来看,实现 super 是不可能的。

那么思考一下,这是什么原理?当一个对象方法运行时,它将当前对象做为 this。若是咱们调用 super.method(),那么如何检索 method?很容易想到,咱们须要从当前对象的原型中取出 method。从技术上讲,咱们(或JavaScript引擎)能够作到这一点吗?

也许咱们能够从 this 的 [[Prototype]] 中得到方法,就像 this .__ proto __.method 同样?不幸的是,这是行不通的。

让咱们试一试,简单起见,咱们不使用 class 了,直接使用普通对象。

在这里,rabbit.eat() 调用父对象的 animal.eat() 方法:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {

    // that's how super.eat() could presumably work
    this.__proto__.eat.call(this); // (*)

  }
};

rabbit.eat(); // Rabbit eats.

(*) 这一行,咱们从原型(animal)中取出 eat,并以当前对象的上下文中调用它。请注意,.call(this) 在这里很重要,由于只写 this .__ proto __.eat() 的话 eat 的调用对象将会是 animal,而不是当前对象。

以上代码的 alert 是正确的。

可是如今让咱们再添加一个对象到原型链中,就要出事了:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  eat() {
    // ...bounce around rabbit-style and call parent (animal) method
    this.__proto__.eat.call(this); // (*)
  }
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // ...do something with long ears and call parent (rabbit) method
    this.__proto__.eat.call(this); // (**)
  }
};


longEar.eat(); // Error: Maximum call stack size exceeded

噢,完蛋!调用 longEar.eat() 报错了!

这缘由一眼可能看不透,但若是咱们跟踪 longEar.eat() 调用,大概就知道为何了。在 (*)(**) 两行中, this 的值是当前对象(longEar)。重点来了:全部方法都将当前对象做为 this,而不是原型或其余东西。

所以,在两行 (*)(**) 中,this.__ proto__ 的值都是 rabbit。他们都调用了 rabbit.eat,因而就这么无限循环下去。

状况如图:

1.在 longEar.eat() 里面,(**) 行中调用了 rabbit.eat,而且this = longEar

// inside longEar.eat() we have this = longEar
this.__proto__.eat.call(this) // (**)
// becomes
longEar.__proto__.eat.call(this)
// that is
rabbit.eat.call(this);

2.而后在rabbit.eat(*) 行中,咱们但愿传到原型链的下一层,可是 this = longEar,因此 this .__ proto __.eat又是 rabbit.eat

// inside rabbit.eat() we also have this = longEar
this.__proto__.eat.call(this) // (*)
// becomes
longEar.__proto__.eat.call(this)
// or (again)
rabbit.eat.call(this);
  1. ...所以 rabbit.eat 在无尽循环调动,没法进入下一层。

这个问题不能简单使用 this 解决。

[[HomeObject]]

为了提供解决方案,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]

当函数被指定为类或对象方法时,其 [[HomeObject]] 属性为该对象。

这实际上违反了 unbind 函数的思想,由于方法记住了它们的对象。而且 [[HomeObject]] 不能被改变,因此这是永久 bind(绑定)。因此在 JavaScript 这是一个很大的变化。

可是这种改变是安全的。 [[HomeObject]] 仅用于在 super 中获取下一层原型。因此它不会破坏兼容性。

让咱们来看看它是如何在 super 中运做的:

let animal = {
  name: "Animal",
  eat() {         // [[HomeObject]] == animal
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {         // [[HomeObject]] == rabbit
    super.eat();
  }
};

let longEar = {
  __proto__: rabbit,
  name: "Long Ear",
  eat() {         // [[HomeObject]] == longEar
    super.eat();
  }
};


longEar.eat();  // Long Ear eats.

每一个方法都会在内部 [[HomeObject]] 属性中记住它的对象。而后 super 使用它来解析原型。

在类和普通对象中定义的方法中都定义了 [[HomeObject]],可是对于对象,必须使用:method() 而不是 "method: function()"

在下面的例子中,使用非方法语法(non-method syntax)进行比较。这么作没有设置 [[HomeObject]] 属性,继承也不起做用:

let animal = {
  eat: function() { // should be the short syntax: eat() {...}
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {
    super.eat();
  }
};


rabbit.eat();  // Error calling super (because there's no [[HomeObject]])

静态方法和继承

class 语法也支持静态属性的继承。

例如:

class Animal {

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }

}

// Inherit from Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbits = [
  new Rabbit("White Rabbit", 10),
  new Rabbit("Black Rabbit", 5)
];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // Black Rabbit runs with speed 5.

如今咱们能够调用 Rabbit.compare,假设继承的 Animal.compare 将被调用。

它是如何工做的?再次使用原型。正如你猜到的那样,extends 一样给 Rabbit 提供了引用到 Animal[Prototype]

因此,Rabbit 函数如今继承 Animal 函数。Animal 自带引用到 Function.prototype[[Prototype]](由于它不 extend 其余类)。

看看这里:

class Animal {}
class Rabbit extends Animal {}

// for static propertites and methods
alert(Rabbit.__proto__ === Animal); // true

// and the next step is Function.prototype
alert(Animal.__proto__ === Function.prototype); // true

// that's in addition to the "normal" prototype chain for object methods
alert(Rabbit.prototype.__proto__ === Animal.prototype);

这样 Rabbit 能够访问 Animal 的全部静态方法。

在内置对象中没有静态继承

请注意,内置类没有静态 [[Prototype]] 引用。例如,Object 具备 Object.definePropertyObject.keys等方法,但 ArrayDate 不会继承它们。

DateObject 的结构:

DateObject 之间毫无关联,他们独立存在,不过 Date.prototype 继承于 Object.prototype,仅此而已。

形成这个状况是由于 JavaScript 在设计初期没有考虑使用 class 语法和继承静态方法。

原生拓展

Array,Map 等内置类也能够扩展。

举个例子,PowerArray 继承自原生 Array

// add one more method to it (can do more)
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

请注意一件很是有趣的事情。像 filtermap 和其余内置方法 - 返回新的继承类型的对象。他们依靠 constructor 属性来作到这一点。

在上面的例子中,

arr.constructor === PowerArray

因此当调用 arr.filter() 时,它自动建立新的结果数组,就像 new PowerArray 同样,因而咱们能够继续使用 PowerArray 的方法。

咱们甚至能够自定义这种行为。若是存在静态 getter Symbol.species,返回新建对象使用的 constructor。

下面的例子中,因为 Symbol.species 的存在,mapfilter等内置方法将返回普通的数组:

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }


  // built-in methods will use this as the constructor
  static get [Symbol.species]() {
    return Array;
  }

}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter creates new array using arr.constructor[Symbol.species] as constructor
let filteredArr = arr.filter(item => item >= 10);


// filteredArr is not PowerArray, but Array

alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

咱们能够在其余 key 使用 Symbol.species,能够用于剥离结果值中的无用方法,或是增长其余方法。

相关文章
相关标签/搜索