类 是用于创建对象的模板。
类创建的对象使用的仍是 对象原型。类只是一种更容易的创建原型链的方法。
类是一种 特殊的函数,只能使用 new 调用,直接调用会报错。
定义类
类实际上是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类也有两种定义方式:类表达式 和 类声明。
与函数表达式类似,类表达式可以是匿名的,或者也可以有一个不同于被赋值给的变量的名称的名字。
不同于函数声明的是,类声明具有与 let 和 const 相同的暂时性死区限制,并且表现得像是没有被提升一样。
定义类使用 class 关键字和花括号,例如:
// 类声明
class MyClass {
}
// 类表达式;类是匿名的,但是它被赋值给了变量
const MyClass = class {
};
// 类表达式;类有它自己的名字
const MyClass2 = class MyClass {
};
花括号内部是 类体。在类体内,有若干特性可用。
class MyClass {
// 构造函数
constructor() {
// 构造函数体
}
// 实例字段
myField = "foo";
// 实例方法
myMethod() {
// myMethod 体
}
// 静态字段
static myStaticField = "bar";
// 静态方法
static myStaticMethod() {
// myStaticMethod 体
}
// 静态块
static {
// 静态初始化代码
}
// 字段、方法、静态字段、静态方法、静态块都可以使用私有形式
#myPrivateField = "bar";
}
定义构造函数
constructor 方法 是用于创建和初始化一个由类创建的对象的特殊方法。一个类只能拥有一个名为 “constructor” 的特殊方法。如果类包含多个 constructor 方法,将会抛出一个 SyntaxError。
构造函数的语法与普通函数完全相同——这意味着你可以使用其他语法,例如剩余参数。
在类的构造函数里,this 的值指向新创建的实例。你可以赋予它新的属性:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
构造函数的行为
就像在类声明外的构造函数一样,它会:
- 创建一个新的对象
- 将对象的原型设置为类的
prototype - 将
this绑定到这个新的对象,你可以在构造函数代码中使用this来引用它 - 执行构造函数中的代码
- 返回这个新的对象
this 的值将自动作为 new 的结果返回。不建议从构造函数中返回任何值——因为如果你返回一个非原始类型的值,它将成为 new 表达式的值,而 this 的值将被丢弃。
省略构造函数
如果你不需要任何特殊的初始化内容,你可以省略构造函数,默认的构造函数会被自动生成。
定义实例方法
类可以作为“模板”,自动将 方法 分配给实例,称为 实例方法。
实例方法被定义在所有实例的原型上。
类中定义的非 constructor 方法会被作为实例方法。
class Color {
constructor(r, g, b) {
this.values = [r, g, b];
}
getRed() {
return this.values[0];
}
}
定义实例字段
字段 使得实例可以获得属性,且它们常常独立于构造函数的参数。
class MyClass {
luckyNumber = Math.random();
}
console.log(new MyClass().luckyNumber); // 0.5
console.log(new MyClass().luckyNumber); // 0.3
字段 几乎等价于将一个属性赋值给 this。例如,上面的例子也可以转换为:
class MyClass {
constructor() {
this.luckyNumber = Math.random();
}
}
定义实例 getter 与 setter
getter 字段 允许我们像访问“实际属性”一样操作某些东西。
class Color {
constructor(r, g, b) {
this.values = [r, g, b];
}
get red() {
return this.values[0];
}
set red(value) {
this.values[0] = value;
}
}
const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 0
如果一个字段仅有一个 getter 而没有 setter,它将是只读的。
class Color {
constructor(r, g, b) {
this.values = [r, g, b];
}
get red() {
return this.values[0];
}
}
const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 255
在严格模式下,red.red = 0 这一行将抛出类型错误:“Cannot set property red of #<Color> which has only a getter”。在非严格模式下,赋值将被静默忽略。
私有属性
私有属性 是一组在类的实例上定义只能在类内部访问的特性。
- 私有方法
- 私有字段
- 私有 getter 与 setter
私有字段 是以 #(井号)开头的标识符。井号是这个字段名的必要部分,这也就意味着私有字段永远不会与公共属性发生命名冲突。为了在类中的任何地方引用一个私有字段,你必须在类体中声明它(你不能在类体外部创建私有字段)。除此之外,私有字段与普通属性几乎是等价的。
class Color {
// 声明:每个 Color 实例都有一个名为 #values 的私有字段。
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
getRed() {
return this.#values[0];
}
setRed(value) {
this.#values[0] = value;
}
}
const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255
在类外访问私有字段会导致语法错误,且该错误可以在早期被捕获(早期语法错误)。因为 #privateField 是一个特殊语法,所以解释器可以在执行代码之前做一些静态分析,找到所有访问私有字段的地方。
JavaScript 中的私有字段是 硬私有的:如果类没有实现暴露这些私有字段的方法,也就没有任何机制可以从类外访问它们。这意味着你可以对类的私有字段做任何重构,只要暴露的方法的行为保持不变即可。
访问其他实例的私有字段
类方法可以读取其他实例的私有字段,只要它们属于同一个类即可。
class Color {
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
redDifference(anotherColor) {
// #values 不一定要从 this 访问:
// 你也可以访问属于同一个类的其他实例的私有字段。
return this.#values[0] - anotherColor.#values[0];
}
}
const red = new Color(255, 0, 0);
const crimson = new Color(220, 20, 60);
red.redDifference(crimson); // 35
然而,若 anotherColor 并非一个 Color 实例,#values 将不存在(即使另一个类有一个同名的私有字段,它也不是同一个东西,也不能在这里访问)。访问一个不存在的私有字段会抛出错误,而不是像普通属性一样返回 undefined。如果你不知道一个对象上是否存在一个私有字段,且你希望在不使用 try/catch 来处理错误的情况下访问它,你可以使用 in 运算符。
class Color {
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
redDifference(anotherColor) {
if (!(#values in anotherColor)) {
throw new TypeError("Color instance expected");
}
return this.#values[0] - anotherColor.#values[0];
}
}
私有字段的限制
有一些限制在使用私有字段时需要注意:在单个类中,相同的名称不能声明两次,且它们不能被删除。这两种情况都会导致早期语法错误。
class BadIdeas {
#firstName;
#firstName; // 这里会发生语法错误
#lastName;
constructor() {
delete this.#lastName; // 也会发生语法错误
}
}
私有方法、私有 getter 与 setter
方法、getter 与 setter 也可以是私有的。当你需要类内部做一些复杂的事情,但是不希望代码的其他部分调用时,它们就很有用。
静态属性
静态属性 是一组在类本身上定义的特性,而不是在类的实例上定义的特性。这些特性包括:
- 静态方法
- 静态字段
- 静态 getter 与 setter
静态属性与实例属性的区别在于:它们有 static 前缀,且它们不能从实例中访问。
静态方法需要直接在类上访问。
class Color {
static isValid(r, g, b) {
return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255;
}
}
Color.isValid(255, 0, 0); // true
Color.isValid(1000, 0, 0); // false
静态方法不能从实例中访问。
console.log(new Color(0, 0, 0).isValid); // undefined
静态初始化块
有一个特殊结构叫做 静态初始化块,它是一个在类第一次加载时运行的代码块。
class MyClass {
static {
MyClass.myStaticProperty = "foo";
}
}
console.log(MyClass.myStaticProperty); // 'foo'
静态初始化块几乎等价于在类声明之后立即执行一些代码。唯一的区别是它们可以访问静态私有属性。
实例化类
在声明一个类之后,你可以使用 new 运算符来创建它的实例。
const myInstance = new MyClass();
console.log(myInstance.myField); // 'foo'
myInstance.myMethod();
典型函数构造器可以使用 new 来构造,也可以不使用 new 来调用。然而,对于类的调用则必须使用 new,否则会导致错误。
const myInstance = MyClass(); // TypeError: Class constructor MyClass cannot be invoked without 'new'
继承
类的一个关键特性是 继承,这意味着一个对象可以“借用”另一个对象的大部分行为,同时覆盖或增强某些部分的逻辑。
派生类 继承自 父类。在 JavaScript 中,派生类是通过 extends 子句声明的,它指示它扩展自哪个类。
class ColorWithAlpha extends Color {
#alpha;
constructor(r, g, b, a) {
super(r, g, b);
this.#alpha = a;
}
get alpha() {
return this.#alpha;
}
set alpha(value) {
if (value < 0 || value > 1) {
throw new RangeError("Alpha 值必须在 0 与 1 之间");
}
this.#alpha = value;
}
}
原型
继承是通过对象原型实现的。
派生类实例的原型对象是派生类的 prototype,派生类的 prototype 的原型是父类的 prototype。所以派生类的实例可以通过原型链访问父类的所有公共属性,包括字段、属性、getter 与 setter。
派生类的实例同时也是父类的实例,可用 instancesof 运算符来验证。
派生类的构造函数
构造函数中可以使用 super 关键字来调用父类的构造函数。
在访问 this 之前,必须调用 super(),这是 JavaScript 的要求。
super() 调用父类的构造函数来初始化 this——这里大致相当于 this = new Color(r, g, b)。super() 之前也可以有代码,但你不能在 super() 之前访问 this——JavaScript 会阻止你访问未初始化的 this。
在父类完成对 this 的修改后,派生类才可以对其进行自己的逻辑。
定义派生类的属性
派生类除了可以访问父类的所有公共属性,还可以添加自己的属性。
例如,上面的代码中添加了一个名为 #alpha 的私有字段,并提供了一对 getter/setter 来与之交互。
覆盖父类的属性
派生类也可以覆盖父类的属性。只需定义与父类同名的属性,就可以覆盖父类的属性。
例如,所有类都隐式继承自 Object 类,它定义了一些基本方法,例如 toString()。然而,基本的 toString() 方法是出了名的无用方法,因为它在大多数情况下打印 [object Object]:
console.log(red.toString()); // [object Object]
所以,我们可以覆盖它,以便在打印颜色时打印它的 RGB 值:
class Color {
#values;
// …
toString() {
return this.#values.join(", ");
}
}
console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'
类内访问父类
在派生类内,你可以使用 super 访问父类的方法。这允许你可以在避免代码重复的情况下增强父类的方法。
class ColorWithAlpha extends Color {
#alpha;
// …
toString() {
// 调用父类的 toString(),并以此构建新的返回值
return `${super.toString()}, ${this.#alpha}`;
}
}
console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'
硬私有
派生类无权访问父类的私有字段——这是 JavaScript 私有字段的一个关键特性(“硬私有”)。私有字段的有效范围被严格限制在类体内,所以任何外部代码都无权访问。
class ColorWithAlpha extends Color {
log() {
console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class
}
}
单继承
一个类只能至多扩展自一个父类。这可以防止多重继承中的问题,例如菱形问题。然而,由于 JavaScript 的动态性,仍然可以通过类组合和 mixin 来实现多重继承的效果。
类定义转化为函数构造器
class MyClass {
// 构造函数
constructor() {
// 构造函数体
}
// 实例字段
myField = "foo";
// 实例方法
myMethod() {
// myMethod 体
}
// 静态字段
static myStaticField = "bar";
// 静态方法
static myStaticMethod() {
// myStaticMethod 体
}
// 静态块
static {
// 静态初始化代码
}
}
以上类定义的代码可以转化为如下的函数构造器:
function MyClass() {
this.myField = "foo";
// 构造函数体
}
MyClass.myStaticField = "bar";
MyClass.myStaticMethod = function () {
// myStaticMethod 体
};
MyClass.prototype.myMethod = function () {
// myMethod 体
};
(function () {
// 静态初始化代码
})();
私有字段和方法是类中的新特性,在函数构造器中并没有与之等价的语法。
参考
学习:JavaScript 中的类
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Classes_in_JavaScript
指南:使用类
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_classes
参考:类
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes
extends
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/extends