原文 https://javascript.info/class...javascript
class 能够 extends 自另外一个 class。这是一个不错的语法,技术上基于原型继承。java
要继承一个对象,须要在 {..}
前指定 extends
和父对象。git
这个 Rabbit
继承自 Animal
:github
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
如今让咱们进入下一步,重写一个方法。到目前为止,Rabbit
从 Animal
继承了 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!
如今,Rabbit
的 stop
方法经过 super.stop()
调用父类的方法。
super
正如在 arrow-functions 一章中提到,箭头函数没有 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);
对于构造函数来讲,这有点棘手 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”
。
区别在于:
因此若是咱们正在构造咱们本身的构造函数,那么咱们必须调用 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
的底层实现,咱们会看到一些有趣的事情。
首先要说的是,以咱们迄今为止学到的知识来看,实现 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);
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.defineProperty
,Object.keys
等方法,但 Array
,Date
不会继承它们。
Date
和 Object
的结构:
Date
和 Object
之间毫无关联,他们独立存在,不过 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
请注意一件很是有趣的事情。像 filter
,map
和其余内置方法 - 返回新的继承类型的对象。他们依靠 constructor
属性来作到这一点。
在上面的例子中,
arr.constructor === PowerArray
因此当调用 arr.filter()
时,它自动建立新的结果数组,就像 new PowerArray
同样,因而咱们能够继续使用 PowerArray 的方法。
咱们甚至能够自定义这种行为。若是存在静态 getter Symbol.species
,返回新建对象使用的 constructor。
下面的例子中,因为 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
咱们能够在其余 key 使用 Symbol.species
,能够用于剥离结果值中的无用方法,或是增长其余方法。