Menu

JavaScript 类教程:构造函数、方法与 new 关键字

JavaScript 类到底是怎么工作的:构造函数、方法、实例字段、getter 和 setter,以及 class 关键字背后的思维模型。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

类是对象的蓝图

类描述了某一种对象的结构和行为。你只需定义一次蓝图,就能按需创建任意多个实例。每个实例都有自己的数据,但共享同一套方法。

基本形式如下:

class User { ... } 定义了蓝图。new User(...) 创建一个实例。每个实例都有自己的 name 和 email,但两者共享类中定义的同一个 greet 方法。

两个实例,两份数据,一套方法。这就是类的全部思想。

constructor 负责初始化每个实例

constructor 是一个特殊的方法,每个实例只运行一次,就在 new 创建它的那一刻。它的任务是初始化这个新对象,通常是把参数复制到 this 上:

构造函数里的 this 指向正在创建的全新实例。赋值 this.x = x 会把 x 放到这个具体的对象上。每次调用 new Point(...) 都会得到自己的 this,因此也有自己的 x 和 y。

如果你不写构造函数,JavaScript 会免费给你一个空的。只有真的需要做初始化工作时,才需要自己写。

new 让整套机制运转起来

不加 new 调用类会抛出错误:

const p = Point(3, 4);
// TypeError: Class constructor Point cannot be invoked without 'new'

这是有意为之。new 会按顺序做四件事:

  1. 创建一个全新的空对象。
  2. 把它链接到类的原型(方法就存放在那里)。
  3. 运行 constructor,并把 this 绑定到这个新对象。
  4. 返回这个新对象。

没有 new,这些都不会发生。类强制执行这条规则,让你不可能不小心忘掉它。这比 class 出现之前的年代进步了很多:那时忘了写 new,会悄无声息地污染全局对象。

方法是共享的,字段属于每个实例

在类体内定义的方法存放在原型上,所有实例共享同一份。实例字段(你赋值给 this 的任何东西)则属于每个实例,每次都是独立的一份。

两个实例都有自己的 count,所以给其中一个加一不会影响另一个。但 a.increment 和 b.increment 就是同一个函数:它只在原型上存一份,每次你在实例上调用时再去查找。这就是类的开销很小的原因:一千个实例并不会把每个方法复制一千份。

类字段

你可以在类体顶部、构造函数之外声明实例字段:

字段声明会在构造函数体之前运行,就好像写在 constructor 的最开头一样。当初始值不依赖构造函数的参数时,它们很好用,比在构造函数里堆满 this.count = 0; this.step = 1; 要清爽。

如果值确实依赖参数,就把赋值留在构造函数里,那里才能访问到参数。

getter 和 setter

getter 或 setter 看起来像方法,用起来却像访问属性。读取或赋值时不用加括号,底层会运行对应的方法:

注意调用方式:t.fahrenheit(不加括号)会读取 getter,t.fahrenheit = 100 会触发 setter。从外部看,fahrenheit 就像一个普通属性,但它是根据 celsius 实时算出来的。

getter 很适合表示派生值。setter 很适合在赋值时校验或规范化值。不要滥用它们:如果一个 getter 开销很大,读代码的人会惊讶于一次“属性访问”竟然在做这么多工作。

方法简写、计算属性名与 this

类中的方法定义使用简写语法:不写 function 关键字,名称和方法体之间也没有 ::

在 add 中返回 this 就能实现链式调用:每次调用都返回同一个实例,下一个方法可以接着在它上面运行。

有一个坑:方法不会自动绑定到它的实例上。如果你把方法单独取出来调用,this 就丢了:

调用 g.hello() 能正常工作,因为 . 提供了 this。单独调用 fn() 就不行。如果你需要一个脱离实例、预先绑定好的方法(事件处理函数里很常见),可以在构造函数中绑定它,或者使用箭头函数形式的类字段:hello = () => \Hi, ${this.name}`;`。

一个可运行的小例子

把各个部分组合起来:字段、构造函数、方法和一个 getter:

一眼就能看懂:这个类准确描述了 BankAccount 是什么、能做什么。

类其实就是函数

有一点值得牢记:class 基本上是语法糖。在底层,类就是一个函数,准确地说是构造函数,而它的方法存放在 ClassName.prototype 上:

typeof User 的结果是 "function"。greet 存放在 User.prototype 上。实例通过原型链查找 greet,这套机制从语言诞生第一天就存在了。类只是给了你一种更清晰的语法来搭建它。

这个思维模型以后会派上用场:只要记住类就是一个函数、方法挂在它的原型上,继承、instanceof 以及调试原型链都会更好理解。

下一步:继承

到目前为止,每个类都是独立的。大多数实际设计都会构建类的层次结构:Dog 是一种 Animal,AdminUser 是一种 User。这由 extends 和 super 关键字来处理,下一篇就会讲到。

常见问题

JavaScript 中如何创建类?

使用 class 关键字加上类名,再写一个代码块,里面放 constructor 方法和其他方法。用 new ClassName(...) 创建实例。例如:class User { constructor(name) { this.name = name; } },然后 new User('Ada')。

JavaScript 类中的 constructor 有什么作用?

调用 new ClassName(...) 时,constructor 会运行一次。它负责初始化新实例,通常是把参数赋值给 this,作为实例属性。如果你不写,JavaScript 会自动提供一个空的默认构造函数。

JavaScript 中类和函数有什么区别?

在底层,类就是函数,准确地说是一个构造函数,它的方法挂在原型上。class 关键字基本上是语法糖,但它强制必须用 new 调用,能简洁地支持 extends,还让方法变成不可枚举的。写新代码时,优先使用 class,而不是手写构造函数。

Coddy 编程语言插图

和 Coddy 一起学编程

开始