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.prototype
到Animal.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
我们能使用在更高级的关键点,如果我们不需要,可以去除一些扩展功能,或许进一步扩展。
本文参考链接:https://blog.csdn.net/neweastsun/article/details/71374659