• 欢迎访问天天编码网站,Java技术、技术书单、开发工具,欢迎加入天天编码
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏天天编码吧
  • 我们的淘宝店铺已经开张了哦,传送门:https://shop145764801.taobao.com/

7.9 类继承,super

JS 教程 tiantian 257次浏览 0个评论 扫描二维码

一个类可以继承另一个类。这是一个非常漂亮的语法,技术上是依赖于原型继承。

为了从另一个类进行继承,我们应该明确"extends"和对应的父类。

下面是Rabbit继承自Animal的示例:

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!

那个extends关键字增加了一个[[Prototype]],从Rabbit.prototype引用到Animal.prototype。就像我们前面所述。

7.9 类继承,super

所以,现在rabbit可以访问它自己的方法,也可以访问Animal的方法。

extends后面可接任何表达式

类的语法不仅仅可以用来定义类,在extends的后面可以定义任意表达式。

举例,一个函数调用可以产生一个父类:

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

class User extends f("Hello") {}

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

此处,class User基础自f("Hello")调用的结果。

这在高级编程可能很有用,因为我们可以使用函数来创建类,这取决于很多的条件。

覆写方法

现在,让我们来继续深入如何覆写方法。目前,RabbitAnimal继承了stop方法,内容是this.speed = 0

如果我们在Rabbit中定义我们自己的stop方法,那么该方法就被使用:

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

但是,一般情况下,我们不是期望完全覆写某一个父类方法,而是在该父类方法的基础上进行改进,减弱或者加强该方法的功能。我们在执行父类方法的基础上/前面/后期,执行一些我们自己的逻辑操作。

类提供了super关键字来达到此目的。

  • super.method(...)调用父类方法。
  • super(...)调用父类构造器方法(只能在本构造器方法内调用)。

举例例子:

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!

现在,Rabbit具有自己的stop方法,该方法在执行过程中会调用父类方法super.stop()

箭头函数没有super

我们在前面的深入箭头函数章节中提到过,箭头函数没有super

如果被访问其箭头函数,那么就从其外部进行获取。举例:

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

箭头函数内的superstop()函数内的super相同,所以该代码可以正常工作。如果我们在此处使用一个普通函数来代理箭头函数,就会导致错误。

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

覆写构造器

对于覆写而言,构造器的行为变得有些古怪。

截至目前,Rabbit都没有它自己的constructor

根据规范,如果一个类继承与另一个类,而且它没有constructor,那么就自动使用如下的constructor

class Rabbit extends Animal {
  // generated for extending classes without own constructors
  constructor(...args) {
    super(...args);
  }
}

正如我们所见,它会以所有的参数来调用其父类的constructor。这在我们没有自己的定义的construcotr时发生。

现在,让我们给Rabbit添加自己的constructor。它除了定义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.

哦!上述的代码产生了一个错误。现在我们无法创建rabbit。为什么?

简单回答是:位于继承类中的构造器必须调用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:internals,[[HomeObject]]

让我们来更加深入地理解super的底层原理。我们可以学到更多有趣的知识。

首先,我们可以问自己一个问题:super在技术上是如何实现的?当某个对象方法运行时,它获取当前对象作this。如果我们调用super.method()方法,如何查询那个method方法?自然地,我们需要从当前对象的原型中获取那个method方法。问题的关键是如何实现呢?

也许我们可以从this[[Prototype]]中获取该方法,比如this.__proto__method?不幸的是,那个方法无法工作。

让我们来尝试该访问。为了简单起见,我们使用简单对象来演示。

此处,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方法,而不是在当前对象上下文中。

在上面的代码中,工作正常:我们获得了正确的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)。

这样的后果就是一个无线循环:

7.9 类继承,super

  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);
  1. rabbit.eat中,代码行(*)处的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]]属性变成了那个对象。

这个性质实际上违反了函数不绑定的原则,因为方法记住了它们的对象。而且[[HomeObject]]不可以被修改,所有这个绑定是永久地。所以这是语言的一个非常重要的改变。

但是这个修改是非常安全的。[[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()

在下面的示例中,使用了一个非方法的语法来进行比较。[[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会被调用。

它是如何工作的?再次使用了原型。你可能已经猜到了,继承的存在使得Rabbit[[Prototype]]引用到Animal

7.9 类继承,super

所以,现在Rabbit函数继承自Animal函数。而且Animal函数具有正常的[[Prototype]]引用到Function.prototype,因为它没有继承人和对象。

我们可以做一个验证:

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.defineProperty,Object.keys等等方法,但是Array,Date等并没有继承他们。

下面看一个DateObject之间的关系:

7.9 类继承,super

注意,在DateObject之间不存在链接。ObjectDate都是独立存在。Date.prototype继承自Object.prototype,仅此而已。

这个差异的存在是因为历史的原因:在 JavaScript 语言发展的早期,不存在任何类和继承的语法,也没有静态方法之类的特性。

原生类是可继承的

内建类,比如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

可以发现非常有趣的事情。内建方法,比如filter,map和其他函数返回的对象的实际类型就是继承后的类型。它们依赖于constructor属性来完成该功能。

在上面的示例中,

arr.constructor === PowerArray

所以,当arr.filter()被调用,它内部创建新结果数组的方式就是new PowerArray。而且,我们在原型链的后面继续使用该方式来创建对象。

此外,我们可以定制化该行为。那个静态的 getter Symbol.species,如果存在,被用来获取此种情况下使用的构造器。

举例,此处因为Symbol.species,内建方法,比如map,filter会返回正常的数组:

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

在更加高级的编程中,我们可以利用该特性在不需要特定的类型时返回正常类型,或者返回任何期望的类型。

任务


无法创建实例:

下面的代码Rabbit继承自Animal

不幸的是,Rabbit对象无法创建,错误在哪里?

class Animal {

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

}

class Rabbit extends Animal {
  constructor(name) {
    this.name = name;
    this.created = Date.now();
  }
}

let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined
alert(rabbit.name);

扩展 clock

我们具有一个 Clock类。现在,它每秒打印时间。

创建一个新类ExtendedClock,继承自Clock类,增加了precision参数,表示多少ms。默认值为1000

  • 代码位于extended-clock.js文件。
  • 不修改原始的clock.js文件。

继承自 Object

正如我们所知,所有对象都继承自Object.prototype,比如可以访问通用的对象方法,比如hasOwnProperty等等。

举例:

class Rabbit {
  constructor(name) {
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

// hasOwnProperty method is from Object.prototype
// rabbit.__proto__ === Object.prototype
alert( rabbit.hasOwnProperty('name') ); // true

但是,如果我们明确的继承呢?比如class Rabbit extends Object,那么它的结果会不会与简单地class Rabbit不同呢?

差异是什么?

下面是一个示例代码(它如何正常工作,为什么?修正它):

class Rabbit extends Object {
  constructor(name) {
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

alert( rabbit.hasOwnProperty('name') ); // true


天天编码 , 版权所有丨本文标题:7.9 类继承,super
转载请保留页面地址:http://www.tiantianbianma.com/class-inherence-super.html/
喜欢 (1)
支付宝[多谢打赏]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址