类
Baseline Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since March 2017.
描述
定义类
类实际上是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类也有两种定义方式:类表达式和类声明。
// 类声明
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
// 类表达式;类是匿名的,但是它被赋值给了变量
const Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
// 类表达式;类有它自己的名字
const Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
与函数表达式类似,类表达式可以是匿名的,或者也可以有一个不同于被赋值给的变量的名称的名字。然而,不同于函数声明的是,类声明具有与 let
和 const
相同的暂时性死区限制,并且表现得像是没有被提升一样。
类主体
类的主体是其被花括号 {}
包裹的部分。这里是你定义方法或构造函数等类成员的地方。
类的主体会执行在严格模式下,即便没有写 "use strict"
指令也一样。
可以从以下三个方面表述一个类元素的特征:
- 种类:getter、setter、方法、字段
- 位置:静态的或位于实例上
- 可见性:公有或私有
它们总共有 16 种可能的组合。为了更合理地划分参考文献,避免内容重叠,不同的元素会在不同的页面进行详细的介绍:
- 方法的定义
-
公有的实例方法
- getter
-
公有的实例 getter 方法
- setter
-
公有的实例 setter 方法
- 类公有字段
-
公有的实例字段
static
-
公有的静态方法、静态 getter、静态 setter 和静态字段
- 私有属性
-
所有私有的元素
备注:私有属性具有在同一个类种声明的所有属性的名称必须唯一的限制。其他所有的公有属性都没有这个限制——你可以写多个同名的公有属性,但是最后一个会覆盖掉其他的。此种表现与对象初始化器相同。
另外,有两种特殊的类元素语法:constructor
和静态初始化块,它们有自己的参考资料。
构造函数
constructor
方法是用于创建和初始化一个由类创建的对象的特殊方法。一个类只能拥有一个名为“constructor”的特殊方法。如果类包含多个 constructor
方法,将会抛出一个 SyntaxError
。
构造函数可以使用 super
关键字来调用父类的构造函数。
你可以在构造方法中创建实例的属性:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
或者,如果实例属性的值不依赖构造函数的参数,那么你可以把它们定义为类字段。
静态初始化块
静态初始化块使静态属性可以灵活初始化,包括在初始化期间执行语句、授予外部对私有作用域的访问权等。
可以声明多个静态块,并且它们可以与静态字段和方法随意穿插(所有的静态项会按照声明顺序被执行或求值)。
方法
方法被定义在类实例的原型上并且被所有实例共享。方法可以是普通函数、异步函数、生成器函数或异步生成器函数。更多信息,参见方法的定义。
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// getter 方法
get area() {
return this.calcArea();
}
// 方法
calcArea() {
return this.height * this.width;
}
*getSides() {
yield this.height;
yield this.width;
yield this.height;
yield this.width;
}
}
const square = new Rectangle(10, 10);
console.log(square.area); // 100
console.log([...square.getSides()]); // [10, 10, 10, 10]
静态方法和字段
static
关键字用来定义类的静态方法或字段。静态属性(字段和方法)被定义在类的自身而不是类的实例上。静态方法通常用于为应用程序创建工具函数,而静态字段则多用于存放缓存、固定配置或其他不需要跨实例复制的数据。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static displayName = "Point";
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
}
const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.displayName; // undefined
p1.distance; // undefined
p2.displayName; // undefined
p2.distance; // undefined
console.log(Point.displayName); // "Point"
console.log(Point.distance(p1, p2)); // 7.0710678118654755
字段声明
使用类字段声明语法,构造函数小节的示例可以写成如下形式:
class Rectangle {
height = 0;
width;
constructor(height, width) {
this.height = height;
this.width = width;
}
}
类字段与对象属性相似,不属于变量,所以我们不需要使用诸如 const
一类的关键字去声明它们。在 JavaScript 中,私有属性使用了一种特殊的语法,所以不应当使用像 public
和 private
这样的修饰符关键字。
如上文所示,声明字段时有没有默认值都可以。没有默认值的字段其值默认为 undefined
。通过预先声明字段,类声明会变得自我文档化,并且字段始终显现,有助于优化代码。
参见公有类字段获取更多信息。
私有属性
使用私有字段,上文的声明可以细化为如下形式:
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
}
从类的外部引用私有字段是错误的,它们只能在类主体的内部被读写。通过定义在类外部不可见的内容,你可以确保类的使用者不能依赖类的内部构造,因为内部构造随着版本更新可能会发生变化。
私有字段只能在字段声明中预先声明。它们不像普通属性那样可以通过赋值创建。
更多信息,参见私有属性。
继承
extends
关键字用于类声明或类表达式中,用以创建一个类作为另一构造函数(类或函数)的子类。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} 发出叫声。`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // 调用父类的构造函数并且传递了 name 参数
}
speak() {
console.log(`${this.name} 吠叫。`);
}
}
const d = new Dog("Mitzie");
d.speak(); // Mitzie 吠叫。
如果子类中定义了构造函数,那么它必须先调用 super()
才能使用 this
。super
关键字也可以用来调用父类中对应的方法。
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} 发出叫声。`);
}
}
class Lion extends Cat {
speak() {
super.speak();
console.log(`${this.name} 吼叫。`);
}
}
const l = new Lion("Fuzzy");
l.speak();
// Fuzzy 发出叫声。
// Fuzzy 吼叫。
求值顺序
当一个 class
声明或 class
表达式被求值,它的各个组件将按照以下顺序被求值:
extends
子句,如果有,将会首先被求值。它必须被求取为一个合法的构造函数或null
,否则将抛出一个TypeError
。- 提取
constructor
方法,如果constructor
不存在将会用默认实现进行替换。但是,因为constructor
的定义仅仅只是一个方法的定义,所以这一步是观察不到的。 - 按照声明顺序对类元素的属性键名求值。如果属性键名是计算属性名,则对表达式求值,表达式中的
this
指向类声明所处上下文的this
(不是类本身),属性值尚不会被求值。 - 按照声明顺序安设方法和访问器。实例方法和访问器被安设在当前类的
prototype
属性上,静态方法和访问器被安设在类本身。私有实例方法和访问器会被保存,之后会直接安置到实例上,这个步骤不可被观察到。 - 类现在已经用
extends
指定的原型和constructor
指定的实现初始化完成。对于上面的所有步骤,如果有表达式尝试访问类名,会抛出一个ReferenceError
,因为类还没有初始化完成。 - 按照声明顺序求取类元素的值:
- 类现在已经被完全初始化并且可以被作为构造函数使用。
对于实例是如何被创建的,请参阅 constructor
文献。
示例
实例方法与静态方法的 this 绑定
当在没有 this
值的情况下调用一个静态方法或实例方法,例如通过将方法赋值给一个变量然后调用,在方法中,this
值将会是 undefined
。即便没有写 "use strict"
指令,表现依然会相同,因为 class
主体中的代码永远是在严格模式下执行的。
class Animal {
speak() {
return this;
}
static eat() {
return this;
}
}
const obj = new Animal();
obj.speak(); // Animal 对象
const speak = obj.speak;
speak(); // undefined
Animal.eat(); // Animal 类
const eat = Animal.eat;
eat(); // undefined
如果我们用传统基于函数的语法去覆写上述方法,那么在非严格模式下,调用方法时,this
会被自动绑定到 globalThis
。在严格模式下,this
值仍然是 undefined
。
function Animal() {}
Animal.prototype.speak = function () {
return this;
};
Animal.eat = function () {
return this;
};
const obj = new Animal();
const speak = obj.speak;
speak(); // 全局对象(在非严格模式下)
const eat = Animal.eat;
eat(); // 全局对象(在非严格模式下)
规范
Specification |
---|
ECMAScript Language Specification # sec-class-definitions |
浏览器兼容性
BCD tables only load in the browser