使用派生类进行继承
ES6
之前,实现自定义类型的继承是个繁琐的过程。严格的继承要求有多个步骤。例如,研究以下范例:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value:Square,
enumerable: false,
writable: true,
configurable: true
}
});
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
Square
继承了 Rectangle
,为此它必须使用 Rectangle.prototype
所创建的一个新对象来重写 Square.prototype
,并且还要调用 Rectangle.call()
方法。这些步骤常常会搞晕 JS
的新手,并会成为有经验开发者出错的根源之一。
类让继承工作变得更轻易,使用熟悉的 extends
关键字来指定当前类所需要继承的函数,即可。生成的类的原型会被自动调整,而你还能调用 super()
方法来访问基类的构造器。此处是与上个例子等价的 ES6
代码:
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
此次 Square
类使用了 extends
关键字继承了 Rectangle
。Square
构造器使用了 super()
配合指定参数调用了 Rectangle
的构造器。注意与 ES5
版本的代码不同,Rectangle
标识符仅在类定义时被使用了(在 extends
之后) 。
继承了其他类的类被称为派生类(derived classes
)。如果派生类指定了构造器,就需要使用 super()
,否则会造成错误。若你选择不使用构造器,super()
方法会被自动调用,并会使用创建新实例时提供的所有参数。例如,下列两个类是完全相同的:
class Square extends Rectangle {
// no constructor
}
// Is equivalent to
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
此例中的第二个类展示了与所有派生类默认构造器等价的写法,所有的参数都按顺序传递给了基类的构造器。在当前需求下,这种做法并不完全准确,因为 Square
构造器只需要单个参数,因此最好手动定义构造器。
使用
|
屏蔽类方法
派生类中的方法总是会屏蔽基类的同名方法。例如,你可以将 getArea()
方法添加到 Square
类,以便重定义它的功能:
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// override and shadow Rectangle.prototype.getArea()
getArea() {
return this.length * this.length;
}
}
由于 getArea()
已经被定义为 Square
的一部分,Rectangle.prototype.getArea()
方法就不能在 Square
的任何实例上被调用。当然,你总是可以使用 super.getArea()
方法来调用基类中的同名方法,就像这样:
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// override, shadow, and call Rectangle.prototype.getArea()
getArea() {
return super.getArea();
}
}
用这种方式使用 super
,其效果等同于第四章讨论过的 super
引用(详见 “使用 super
引用的简单原型访问”)。this
值会被自动设置为正确的值,因此你就能进行简单的调用。
继承静态成员
如果基类包含静态成员,那么这些静态成员在派生类中也是可用的。继承的工作方式类似于其他语言,但对于 JS
而言则是新概念。此处有个范例:
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
static create(length, width) {
return new Rectangle(length, width);
}
}
class Square extends Rectangle {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); // 12
console.log(rect instanceof Square); // false
在此代码中,一个新的静态方法 create()
被添加到 Rectangle
类中。通过继承,该方法会以 Square.create()
的形式存在,并且其行为方式与 Rectangle.create()
一样。
从表达式中派生类
在 ES6
中派生类的最强大能力,或许就是能够从表达式中派生类。只要一个表达式能够返回一个具有 [[Construct]]
属性以及原型的函数,你就可以对其使用 extends
。例如:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
Rectangle
被定义为 ES5
风格的构造器,而 Square
则是一个类。由于 Rectangle
具有 [[Construct]]
以及原型,Square
类就能直接继承它。
extends
后面能接受任意类型的表达式,这带来了巨大可能性,例如动态地决定所要继承的类:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function getBase() {
return Rectangle;
}
class Square extends getBase() {
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
getBase()
函数作为类声明的一部分被直接调用,它返回了 Rectangle
,使得此例的功能等价于前一个例子。并且由于可以动态地决定基类,那也就能创建不同的继承方式。例如,你可以有效地创建混入:
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"
此例使用了混入(mixin
)而不是传统继承。mixin()
函数接受代表混入对象的任意数量的参数,它创建了一个名为 base
的函数,并将每个混入对象的属性都赋值到新函数的原型上。此函数随后被返回,于是 Square
就能够对其使用 extends
关键字了。注意由于仍然使用了 extends
,你就必须在构造器内调用 super()
。
Square
的实例既有来自 AreaMixin
的 getArea()
方法,又有来自 SerializableMixin
的 serialize()
方法,这是通过原型继承实现的。mixin()
函数使用了混入对象的所有自有属性,动态地填充了新函数的原型(记住:若多个混入对象拥有相同的属性,则只有最后添加的属性会被保留)。
任意表达式都能在
试图使用结果为上述值的表达式来创建一个新的类实例,都会抛出错误,因为不存在 |
继承内置对象
几乎从 JS
数组出现那天开始,开发者就想通过继承机制来创建他们自己的特殊数组类型。在 ES5
及早期版本中,这是不可能做到的。试图使用传统继承并不能产生功能正确的代码,例如:
// built-in array behavior
var colors = [];
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
// trying to inherit from array in ES5
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]); // "red"
console.log()
在此代码尾部的输出说明了:对数组使用传统形式的 JS
继承, 产生了预期外的行为。MyArray
实例上的 length
属性以及数值属性,其行为与内置数组并不一致,因为这些功能并未被涵盖在 Array.apply()
或数组原型中。
在 ES6
中的类,其设计目的之一就是允许从内置对象上进行继承。为了达成这个目的,类的继承模型与 ES5
或更早版本的传统继承模型有轻微差异:
在 ES5
的传统继承中,this
的值会先被派生类(例如 MyArray
)创建,随后基类构造器(例如 Array.apply()
方法)才被调用。这意味着 this
一开始就是 MyArray
的实例,之后才使用了 Array
的附加属性对其进行了装饰。
在 ES6
基于类的继承中,this
的值会先被基类(Array
)创建,随后才被派生类的构造器(MyArray
)所修改。结果是 this
初始就拥有作为基类的内置对象的所有功能,并能正确接收与之关联的所有功能。
以下范例实际展示了基于类的特殊数组:
class MyArray extends Array {
// empty
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
MyArray
直接继承了 Array
,因此工作方式与正规数组一致。与数值索引属性的互动更新了 length
属性,而操纵 length
属性也能更新索引属性。这意味着你既能适当地继承 Array
来创建你自己的派生数组类,也同样能继承其他的内置对象。伴随着这些附加功能,ES6
与派生类型有效解决了从内置类型进行派生这最后的特殊情况,不过这种情况仍然值得继续探索。
Symbol.species 属性
继承内置对象一个有趣的方面是:任意能返回内置对象实例的方法,在派生类上却会自动返回派生类的实例。因此,若你拥有一个继承了 Array
的派生类 MyArray
,诸如 slice()
之类的方法都会返回 MyArray
的实例。例如:
class MyArray extends Array {
// empty
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true
在此代码中,slice()
方法返回了 MyArray
的一个实例。slice()
方法是从 Array
上继承的,原本应当返回 Array
的一个实例。而 Symbol.species
属性在后台造成了这种变化。
Symbol.species
知名符号被用于定义一个能返回函数的静态访问器属性。每当类实例的方法(构造器除外)必须创建一个实例时,前面返回的函数就被用为新实例的构造器。下列内置类型都定义了 Symbol.species
:
-
Array
-
ArrayBuffer ( 详见第十章)
-
Map
-
Promise
-
RegExp
-
Set
-
类型化数组( 详见第十章)
以上每个类型都拥有默认的 Symbol.species
属性,其返回值为 this
,意味着该属性总是会返回自身的构造器函数。若你准备在一个自定义类上实现此功能,代码就像这样:
// several builtin types use species similar to this
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
在此例中,Symbol.species
知名符号被用于定义 MyClass
的一个静态访问器属性。注意此处只有 getter
而没有 setter
,这是因为修改类的 species
是不允许的。任何对 this.constructor[Symbol.species]
的调用都会返回 MyClass
,clone()
方法使用了该定义来返回一个新的实例,而没有直接使用 MyClass
,这就允许派生类重写这个值。例如:
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
// empty
}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("bar"),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false
此处, MyDerivedClass1
继承了 MyClass
,并且未修改 Symbol.species
属性。由于 this.constructor[Symbol.species]
会返回 MyDerivedClass1
,当 clone()
被调用时,它就返回了 MyDerivedClass1
的一个实例。MyDerivedClass2
类也继承了 MyClass
,但重写了 Symbol.species
,让其返回 MyClass
。当 clone()
在 MyDerivedClass2
的一个实例上被调用时,返回值就变成 MyClass
的一个实例。使用 Symbol.species
,任意派生类在调用应当返回实例的方法时,都可以判断出需要返回什么类型的值。
例如,Array
使用了 Symbol.species
来指定方法所使用的类,让其返回值为一个数组。在 Array
派生出的类中,你可以决定这些继承的方法应返回何种类型的对象,正如:
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof Array); // true
console.log(subitems instanceof MyArray); // false
此代码重写了从 Array
派生的 MyArray
类上的 Symbol.species
。所有返回数组的继承方法现在都会使用 Array
的实例,而不是 MyArray
的实例。
一般而言,每当想在类方法中使用 this.constructor
时,你就应当设置类的 Symbol.species
属性。这么做允许派生类轻易地重写方法的返回类型。此外,若你从一个拥有 Symbol.species
定义的类创建了派生类,要保证使用此属性,而不是直接使用构造器。