extends
试一试
语法
class ChildClass extends ParentClass { /* … */ }
父类
-
一个表达式,它计算结果为构造函数(包括类)或
null
。
描述
extends
关键字可用于对自定义类以及内置对象进行子类化。
任何可以使用 new
调用的并具有 prototype
属性的构造函数都可以作为父类的候选者。这两个条件必须都成立——例如,绑定函数 和 Proxy
可以被构造,但它们没有 prototype
属性,因此它们不能被子类化。
function OldStyleClass() {
this.someProperty = 1;
}
OldStyleClass.prototype.someMethod = function () {};
class ChildClass extends OldStyleClass {}
class ModernClass {
someProperty = 1;
someMethod() {}
}
class AnotherChildClass extends ModernClass {}
ParentClass
的 prototype
属性必须是 Object
或 null
,但在实践中你很少会担心这一点,因为非对象 prototype
不会按预期工作。(它会被 new
运算符忽略。)
function ParentClass() {}
ParentClass.prototype = 3;
class ChildClass extends ParentClass {}
// Uncaught TypeError: Class extends value does not have valid prototype property 3
console.log(Object.getPrototypeOf(new ParentClass()));
// [Object: null prototype] {}
// Not actually a number!
extends
设置了 ChildClass
和 ChildClass.prototype
的原型。
ChildClass 的原型 |
ChildClass.prototype 的原型 |
|
---|---|---|
缺少 extends 子句 |
Function.prototype |
Object.prototype |
extends null |
Function.prototype |
null |
extends ParentClass |
父类 |
ParentClass.prototype |
class ParentClass {}
class ChildClass extends ParentClass {}
// Allows inheritance of static properties
Object.getPrototypeOf(ChildClass) === ParentClass;
// Allows inheritance of instance properties
Object.getPrototypeOf(ChildClass.prototype) === ParentClass.prototype;
extends
的右侧不必是标识符。您可以使用任何计算结果为构造函数的表达式。这通常用于创建 mixin。extends
表达式中的 this
值是围绕类定义的 this
,并且引用类的名称是一个 ReferenceError
,因为类尚未初始化。 await
和 yield
在此表达式中按预期工作。
class SomeClass extends class {
constructor() {
console.log("Base class");
}
} {
constructor() {
super();
console.log("Derived class");
}
}
new SomeClass();
// Base class
// Derived class
虽然基类可以从其构造函数返回任何内容,但派生类必须返回一个对象或 undefined
,否则将抛出 TypeError
。
class ParentClass {
constructor() {
return 1;
}
}
console.log(new ParentClass()); // ParentClass {}
// The return value is ignored because it's not an object
// This is consistent with function constructors
class ChildClass extends ParentClass {
constructor() {
super();
return 1;
}
}
console.log(new ChildClass()); // TypeError: Derived constructors may only return object or undefined
如果父类构造函数返回一个对象,则在进一步初始化 类字段 时,该对象将用作派生类的 this
值。此技巧称为 "返回覆盖",它允许在不相关对象上定义派生类的字段(包括 私有 字段)。
子类化内置对象
警告:标准委员会现在认为先前规范版本中的内置子类化机制设计过度,并导致非可忽略的性能和安全影响。新的内置方法更少考虑子类,并且引擎实现者正在 调查是否要删除某些子类化机制。在增强内置对象时,请考虑使用组合而不是继承。
以下是一些在扩展类时可能遇到的情况
- 当在子类上调用静态工厂方法(如
Promise.resolve()
或Array.from()
)时,返回的实例始终是子类的实例。 - 当在子类上调用返回新实例的实例方法(如
Promise.prototype.then()
或Array.prototype.map()
)时,返回的实例始终是子类的实例。 - 实例方法尝试尽可能地委托给最少的原始方法集。例如,对于
Promise
的子类,覆盖then()
会自动导致catch()
的行为发生变化;或者对于Map
的子类,覆盖set()
会自动导致Map()
构造函数的行为发生变化。
但是,上述期望需要付出非微不足道的努力才能正确实现。
- 第一个需要静态方法读取
this
的值以获取用于构造返回实例的构造函数。这意味着[p1, p2, p3].map(Promise.resolve)
会抛出错误,因为Promise.resolve
内部的this
是undefined
。解决此问题的一种方法是如果this
不是构造函数,则回退到基类,就像Array.from()
所做的那样,但这仍然意味着基类是特殊情况。 - 第二个需要实例方法读取
this.constructor
以获取构造函数。但是,new this.constructor()
可能会破坏旧代码,因为constructor
属性既可写又可配置,并且不受任何保护。因此,许多复制内置方法改为使用构造函数的[Symbol.species]
属性(默认情况下仅返回this
,即构造函数本身)。但是,[Symbol.species]
允许运行任意代码并创建任意类型的实例,这会带来安全问题,并且极大地增加了子类化的语义复杂性。 - 第三个会导致自定义代码的可见调用,这使得许多优化难以实现。例如,如果
Map()
构造函数被调用时带有包含 x 个元素的可迭代对象,则它必须可见地调用 x 次set()
方法,而不是仅仅将元素复制到内部存储中。
这些问题并非内置类独有。对于你自己的类,你可能也需要做出同样的决定。但是,对于内置类,可优化性和安全性是一个更大的问题。新的内置方法始终构造基类并尽可能少地调用自定义方法。如果你想在实现上述期望的同时对内置对象进行子类化,你需要覆盖所有具有默认行为烘焙到其中的方法。在基类上添加任何新方法也可能会破坏子类的语义,因为它们是默认继承的。因此,扩展内置对象的一种更好的方法是使用 组合。
扩展 null
extends null
旨在允许轻松创建 不继承自 Object.prototype
的对象。但是,由于关于是否应在构造函数内调用 super()
的决定尚未确定,因此在实践中无法使用任何不返回对象的构造函数实现来构造这样的类。 TC39 委员会正在努力重新启用此功能。
new (class extends null {})();
// TypeError: Super constructor null of anonymous class is not a constructor
new (class extends null {
constructor() {}
})();
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
new (class extends null {
constructor() {
super();
}
})();
// TypeError: Super constructor null of anonymous class is not a constructor
相反,你需要从构造函数中显式地返回一个实例。
class NullClass extends null {
constructor() {
// Using new.target allows derived classes to
// have the correct prototype chain
return Object.create(new.target.prototype);
}
}
const proto = Object.getPrototypeOf;
console.log(proto(proto(new NullClass()))); // null
示例
使用 extends
第一个示例从名为 Polygon
的类创建了一个名为 Square
的类。此示例摘自此 在线演示 (源代码)。
class Square extends Polygon {
constructor(length) {
// Here, it calls the parent class' constructor with lengths
// provided for the Polygon's width and height
super(length, length);
// Note: In derived classes, super() must be called before you
// can use 'this'. Leaving this out will cause a reference error.
this.name = "Square";
}
get area() {
return this.height * this.width;
}
}
扩展普通对象
类不能扩展常规(不可构造)对象。如果你想通过使该对象的所有属性在继承的实例上可用,来从常规对象继承,则可以使用 Object.setPrototypeOf()
const Animal = {
speak() {
console.log(`${this.name} makes a noise.`);
},
};
class Dog {
constructor(name) {
this.name = name;
}
}
Object.setPrototypeOf(Dog.prototype, Animal);
const d = new Dog("Mitzie");
d.speak(); // Mitzie makes a noise.
扩展内置对象
扩展 Object
默认情况下,所有 JavaScript 对象都从 Object.prototype
继承,因此乍一看 extends Object
似乎是多余的。与完全不写 extends
的唯一区别在于,构造函数本身从 Object
继承静态方法,例如 Object.keys()
。但是,由于没有 Object
静态方法使用 this
值,因此继承这些静态方法仍然没有价值。
Object()
构造函数对子类化场景进行了特殊处理。如果它通过 super()
隐式调用,它始终使用 new.target.prototype
作为其原型初始化一个新对象。传递给 super()
的任何值都将被忽略。
class C extends Object {
constructor(v) {
super(v);
}
}
console.log(new C(1) instanceof Number); // false
console.log(C.keys({ a: 1, b: 2 })); // [ 'a', 'b' ]
将此行为与不特殊处理子类化的自定义包装器进行比较
function MyObject(v) {
return new Object(v);
}
class D extends MyObject {
constructor(v) {
super(v);
}
}
console.log(new D(1) instanceof Number); // true
种类
你可能希望在派生的数组类 MyArray
中返回 Array
对象。种类模式允许你覆盖默认构造函数。
例如,当使用诸如 Array.prototype.map()
之类的返回默认构造函数的方法时,你希望这些方法返回父 Array
对象,而不是 MyArray
对象。 Symbol.species
符号允许你执行此操作
class MyArray extends Array {
// Overwrite species to the parent Array constructor
static get [Symbol.species]() {
return Array;
}
}
const a = new MyArray(1, 2, 3);
const mapped = a.map((x) => x * x);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
许多内置复制方法都实现了此行为。有关此功能的注意事项,请参阅 内置子类化 讨论。
混合
抽象子类或混合是类的模板。一个类只能有一个超类,因此例如,从工具类的多重继承是不可能的。功能必须由超类提供。
可以使用一个函数,该函数以超类作为输入,并以扩展该超类的子类作为输出,来实现混合。
const calculatorMixin = (Base) =>
class extends Base {
calc() {}
};
const randomizerMixin = (Base) =>
class extends Base {
randomize() {}
};
然后可以这样编写使用这些混合的类
class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}
避免继承
在面向对象编程中,继承是一种非常强的耦合关系。这意味着基类的所有行为都默认由子类继承,这可能并不总是你想要的。例如,考虑 ReadOnlyMap
的实现
class ReadOnlyMap extends Map {
set() {
throw new TypeError("A read-only map must be set at construction time.");
}
}
事实证明,ReadOnlyMap
是不可构造的,因为 Map()
构造函数调用实例的 set()
方法。
const m = new ReadOnlyMap([["a", 1]]); // TypeError: A read-only map must be set at construction time.
我们可以通过使用一个私有标志来指示实例是否正在构造来解决这个问题。但是,此设计的一个更重要的问题是它违反了 里氏替换原则,该原则指出子类应该可以替换其超类。如果一个函数期望一个 Map
对象,它也应该能够使用 ReadOnlyMap
对象,这将在此处失效。
继承通常会导致 圆形-椭圆形问题,因为这两种类型都不完全包含对方的行为,尽管它们有很多共同的特征。一般来说,除非有充分的理由使用继承,否则最好使用组合。组合意味着一个类对另一个类的对象的引用,并且仅将该对象用作实现细节。
class ReadOnlyMap {
#data;
constructor(values) {
this.#data = new Map(values);
}
get(key) {
return this.#data.get(key);
}
has(key) {
return this.#data.has(key);
}
get size() {
return this.#data.size;
}
*keys() {
yield* this.#data.keys();
}
*values() {
yield* this.#data.values();
}
*entries() {
yield* this.#data.entries();
}
*[Symbol.iterator]() {
yield* this.#data[Symbol.iterator]();
}
}
在这种情况下,ReadOnlyMap
类不是 Map
的子类,但它仍然实现了大多数相同的方法。这意味着更多的代码重复,但也意味着 ReadOnlyMap
类没有与 Map
类紧密耦合,并且如果 Map
类发生更改,不会轻易失效,从而避免了 内置子类化的语义问题。例如,如果 Map
类添加了一个 emplace()
方法,该方法不调用 set()
,则会导致 ReadOnlyMap
类不再是只读的,除非后者相应地更新以覆盖 emplace()
。此外,ReadOnlyMap
对象根本没有 set
方法,这比在运行时抛出错误更准确。
规范
规范 |
---|
ECMAScript 语言规范 # sec-class-definitions |
浏览器兼容性
BCD 表格仅在浏览器中加载
另请参阅
- 使用类 指南
- 类
constructor
class
super