Javascript面向对象(九)——类继承

Javascript面向对象(九)——类继承

类可以继承另一个类。有漂亮的语法,技术上基于原型继承。

为了从另一个类继承,我们应该指定“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关键字准确地增加一个原型引用从Rabbit.prototypeAnimal.prototype,正如你期望的一样,下面图示我们之前也见过:

所以现在rabbit对象能访问自己的方法,也可以访问来自父类Animal的方法。

任何表达式都可以在extends之后

继承语法允许继承不仅是类,在extens之后可以为任意表达式。
示例,一个函数调用生成父类:

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

class User extends f("Hello") {}

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

这里类User 继承自f(“hello”)函数的调用结果。
这可能在一些高级编程模型中非常有用,我们使用函数依赖很多条件生成类,然后可以从它们继承。

方法覆盖

现在让我们继续学习方法覆盖。截至目前,Rabbit继承了Animal类的stop方法,并设置this.speed = 0
如果我们在Rabbit中指定我们自己的方法stop,那么则会覆盖父类方法并使用。

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

但通常我们并不想完全替代父类的方法,而是构建在其之上,稍微调整或扩展其功能。我们实现在实现方法中增加一些功能,但这过程中或前后调用父类的方法。Javascript提供了关键自“super”可以实现。

  • super.method(…) 调用父类方法
  • super(…) 调用父类构造函数(只能在构造函数内部调用)

举例,让rabbit对象调用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
    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
  }
}

这里super在箭头函数中,和在stop()方法是一样的,所以如我们期望的一样。
如果我们在一般的函数中使用,会抛出错误:

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

覆盖构造函数

构造函数覆盖有点棘手。
到目前为止,Rabbit类没有自己的构造函数。根据规范,如果一个类继承自另一个类,没有提供构造函数,那么自动生成一个构造函数:

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

我们看到,基本上是调用父类构造函数,并传入所有参数。因为如果我们不写构造函数,就会这样。
现在,我们增加自己的构造函数,在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:内部的,[[HomeObject]]
让我们深入super的底层,顺便可以看到一些有趣的事情。

首先要说的,从我们已经学习的内容,super不可能实现。

是的,确实是的,让我们问自己,技术上super是如何工作的?当对象方法执行是,系统获得当前对象作为this。
如果我们调用super.method()时,如何返回方法?换句话说,我们需要从当前对象的父原型中获得方法,那么Javascript引擎在技术上是如何实现的?

可能我们获得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() {
    this.__proto__.eat.call(this); // (*)
  }
};

rabbit.eat(); // Rabbit eats.

在型号行,我们从原型(animal)中获得eat方法,并在当前对象的上下文中调用它。请注意.call(this)是很重要的,因为简单的this.__proto__.eat()执行原型上下文中的父对象的eat方法,不是当前对象。这里一切如愿。

现在让我们增加更多的链,我们将发现意外:

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)
// or
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);

3、所以rabbit.eat调用自身,进入死循环,因为它不能进一步向上执行。

这个问题是无法解决,因为this必须总是调用对象自身,无论那个父对象调用。所以它的原型总是直接父对象,我们不能向上获得原型链。

[[HomeObject]]

为了提供解决方案,Javascript给函数增加一个特殊内部属性:[[HomeObject]].

当一个函数被指定作为类或对象方法时,它的[[HomeObject]]属性为那个对象。

这实际上违背了函数“不绑定”的思想,因为方法记住对应的对象,[[HomeObject]]不能被改变,所以这绑定是永久的,所以这是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()方式。

在下面示例中,使用非方法语法用于比较,[[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 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.
如何工作的呢?使用原型,你可能已经猜到了。扩展也给Rabbit的属性[[Prototype]]引用Animal。

所以,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等没有继承它们。
这里是Date和Object的图示结构:

注意,Date和Object之间没有链接,两者之间独立存在。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

注意一个有趣的事情,内置方法如fliter,map等返回正是继承类型(子类)的对象,依赖构造器属性实现。
上面示例中:
arr.constructor === PowerArray

所以当arr.filter()被调用时,内部正是通过new PowerArray创建新数组,我们可以使用原型链中更深层的方法。
而且,我们可以自定义行为,静态getter Symbol.species,如果存在,返回使用的构造器。

举例,这里由于Symbol.species,内置方法如map,fliter将返回正常数组:

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

我们能使用在更高级的关键点,如果我们不需要,可以去除一些扩展功能,或许进一步扩展。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值