菜单

相关源文件

本文档解释了 JavaScript 的类系统,该系统已在 You-Dont-Know-JS 代码库中实现。它涵盖了 class 关键字及其相关机制的语法、行为和特性。有关作为类替代方案的委托模式的信息,请参阅 委托

JavaScript 中的类概述

JavaScript 的类系统提供了一种定义蓝图的方法,用于创建具有特定数据(成员)和行为(方法)的对象。尽管 JavaScript 一直是基于原型的,但 ES6(2015)中引入的 class 语法为开发人员提供了更熟悉的方式来处理面向对象概念。

最初只是 JavaScript 原型继承之上的语法糖,现在已经发展成为一种具有专用语法和语义的强大功能。重要的是要理解,尽管有类风格的语法,JavaScript 的实现实际上仍在底层使用 [[Prototype]] 机制。

来源: objects-classes/ch3.md8-18

何时使用类

面向类的设计是一种组织程序信息和行为的模式。它最适用于以下情况:

  1. 您需要将域建模为具有共同特征的分类实体
  2. 您的程序需要创建和管理相似对象的多个实例
  3. 您从对象类型的继承关系中受益

类并非所有编程任务的通用解决方案。当您需要处理具有共同行为的同一类型实体的多个实例时,类最为有用。

来源: objects-classes/ch3.md20-64

类语法和结构

JavaScript 类可以定义为声明或表达式

类声明和表达式

类声明使用以下语法

类表达式可以是命名的,也可以是匿名的

在类体中,方法定义时没有 function 关键字,并且方法定义之间没有分隔符(,;)。

类体内的所有代码都会自动以严格模式运行,这会影响 this 的行为。

来源: objects-classes/ch3.md66-107

构造函数

构造函数是一个特殊的函数,当创建类的 new 实例时会被调用。如果省略,JavaScript 会提供一个默认的空构造函数。

当您定义一个类时,JavaScript 会创建一个以类名为名的函数。这个函数就是构造函数

此构造函数必须使用 new 关键字调用;尝试不带 new 调用它将导致错误。

来源: objects-classes/ch3.md109-162

类方法

在类中定义的方法会被添加到构造函数的原型上

在此示例中,setX 定义在 Point2d.prototype 上,而不是直接定义在每个实例上。当在实例上调用方法时,JavaScript 会遍历原型链来查找并执行该方法。

方法通常只应通过实例调用;直接调用 Point2d.setX() 会失败,因为该方法存在于原型上,而不是构造函数本身。

来源: objects-classes/ch3.md176-202

类实例和 this

在类方法(包括构造函数)中,this 关键字指向当前实例

xy 这样保存非函数值的属性称为成员,而可执行的函数称为方法。

来源: objects-classes/ch3.md204-233

公共字段

ES2022 引入了公共字段,它允许在类体中直接声明性地定义实例成员

公共字段可以用值进行初始化,并且可以通过 this 相互引用

字段名也可以是计算的

您可以将公共字段声明视为它们出现在构造函数的顶部,每个都以隐含的 this. 作为前缀。

来源: objects-classes/ch3.md235-294

类继承

JavaScript 中的类继承使用 extends 关键字实现

使用 extends

extends 关键字在两个类之间建立关系

在此示例中,Point3dPoint2d 的子类(派生类)。子类从基类继承方法和属性,可以用新定义覆盖它们,并可以添加自己独特的​​方法和属性。

来源: objects-classes/ch3.md374-416

方法重写

子类可以覆盖父类的方法

即使方法被重写,您仍然可以使用 super 访问父类实现

方法在不同继承级别上表现出不同行为的能力称为方法多态。

来源: objects-classes/ch3.md421-486

使用 super

在子类中,super 关键字有两个用途

  1. 作为函数调用(super(...)),用于调用父构造函数
  2. 作为对象引用(super.method()),用于访问父类方法

子类构造函数在访问 this 或返回之前,必须调用 super()

如果您在子类中省略构造函数,JavaScript 会自动创建一个调用 super() 的构造函数,并将所有参数传递过去。

来源: objects-classes/ch3.md487-521

子类中的字段初始化

当您在具有显式构造函数的子类中定义字段时,字段初始化发生在 super() 调用之后,但在构造函数其余代码执行之前。

来源: objects-classes/ch3.md521-559

类型检查和 instanceof

您可以使用 instanceof 运算符检查一个对象是否是特定类的实例

instanceof 运算符检查整个原型链,而不仅仅是直接构造函数。这就是为什么 anotherPoint instanceof Point2dtrue,尽管它是由 Point3d 创建的。

要检查实例的直接构造函数,请使用 constructor 属性

来源: objects-classes/ch3.md598-658

继承是共享,而不是复制

重要的是要理解,JavaScript 的继承不会将方法和属性从父类复制到子类。相反,它会建立原型链进行属性查找

当您在实例上访问方法时,JavaScript 会在对象本身上查找该方法。如果找不到,它会沿着原型链向上查找,直到找到该方法或到达链的末端。

这种共享机制比将方法复制到每个实例或子类更节省内存。

来源: objects-classes/ch3.md659-708

静态类行为

静态属性和方法属于类本身,而不是类的实例

静态属性和方法

静态成员使用 static 关键字定义

静态属性和方法直接在类上访问,而不是在实例上。它们对于与类相关的实用函数或应在所有实例之间共享的值很有用。

静态属性在类定义后立即初始化,并且初始化顺序很重要。

来源: objects-classes/ch3.md709-768

静态初始化块

ES2022 引入了静态初始化块,它允许对静态属性进行更复杂的初始化

静态初始化块对于需要多条语句的计算或在初始化期间处理异常很有用。

来源: objects-classes/ch3.md769-823

静态继承

静态属性和方法会被子类继承

在子类的静态方法或初始化块中,this 指向子类构造函数,super 指向父类。

来源: objects-classes/ch3.md824-881

与原型的关系

尽管 class 语法提供了一种更简洁的方式来定义构造函数和原型,但理解底层原型机制很重要

当你定义一个类时

  1. JavaScript 会创建一个以类名为名的构造函数
  2. 类中定义的方法会被添加到构造函数的原型上
  3. 静态属性和方法会直接添加到构造函数上
  4. 对于子类,原型链会建立起来以连接构造函数及其原型

当你使用 new 创建实例时

  1. 会创建一个新对象
  2. 对象的 [[Prototype]] 会被设置为构造函数的原型
  3. 构造函数会被调用,并将 this 设置为新对象
  4. 如果构造函数不返回自己的对象,则会返回新对象

这种机制允许方法在实例之间共享,同时每个实例维护自己的状态(属性)。

来源: objects-classes/ch3.md8-18 objects-classes/ch3.md659-708

总结

JavaScript 的类系统提供了一种熟悉的面向对象编程语法,同时仍在底层使用 JavaScript 的基于原型的继承。主要特性包括:

  1. 类声明和表达式
  2. 用于初始化的构造函数
  3. 实例方法和属性
  4. 用于声明性成员定义的公共字段
  5. 通过 extends 关键字实现的继承
  6. 方法重写和使用 super 访问父方法
  7. 类级别的静态属性和方法
  8. 用于共享类级别功能的静态继承

虽然类(classes)非常强大,但在 JavaScript 程序中应谨慎使用它们。当您需要表示具有多个实例且这些实例共享通用行为的分类实体时,并且当继承关系对您的领域有意义时,可以考虑使用类。

来源: objects-classes/ch3.md8-18 objects-classes/ch3.md20-64 objects-classes/toc.md28-35