在 ES6 之前,JavaScript 通过构造函数和原型来模拟传统的面向对象编程。ES6 引入了 class 关键字,让代码写法更接近 Java、C++ 等语言的“类”,但它的底层仍然是原型继承。理解这一点至关重要:class 并不是新的继承模型,而是一层更清晰、更易读的语法糖。
11.6.1 类的定义与构造函数
使用 class 关键字定义一个类,内部必须有一个 constructor 方法(如果没有显式定义,引擎会自动添加一个空构造函数)。对象的实例化仍然使用 new 关键字。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`你好,我是${this.name},今年${this.age}岁。`);
}
}
const alice = new Person('Alice', 25);
alice.greet(); // 你好,我是Alice,今年25岁。
等价于传统的构造函数写法:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`你好,我是${this.name},今年${this.age}岁。`);
};
可见,class 中的方法仍然定义在 Person.prototype 上,实例通过原型链访问它们。
11.6.2 实例方法与静态方法
- 实例方法:直接定义在
class内部的方法,会被添加到原型对象上,所有实例共享。 - 静态方法:使用
static关键字定义的方法,属于类本身,直接通过类名调用,不能通过实例调用。常用于工具函数、工厂方法等。
class MathUtils {
static add(a, b) {
return a + b;
}
multiply(a, b) {
return a * b;
}
}
console.log(MathUtils.add(3, 5)); // 8
const utils = new MathUtils();
console.log(utils.multiply(3, 5)); // 15
// utils.add(3, 5); // 报错:utils.add is not a function
静态属性也可以在类内部直接定义(较新语法):
class Config {
static version = '1.0.0';
}
console.log(Config.version); // 1.0.0
11.6.3 实例属性的新写法
除了在 constructor 中通过 this 定义实例属性外,ES6+ 允许直接在类体中声明实例属性,无需 constructor 包裹,这样可以让代码更简洁(尤其是配合 TypeScript 的类型标注)。
class User {
name = '默认用户名'; // 直接声明实例属性,等价于在 constructor 中 this.name = ...
#age = 0; // 私有属性(见后文)
constructor(name) {
if (name) {
this.name = name;
}
}
}
11.6.4 getter 与 setter
使用 get 和 set 关键字可以为属性定义访问器,在读取或赋值时执行逻辑,常用于数据校验或计算属性。
class Temperature {
constructor(celsius) {
this._celsius = celsius; // 通常用下划线前缀表示内部属性
}
get fahrenheit() {
return this._celsius * 9 / 5 + 32;
}
set fahrenheit(value) {
this._celsius = (value - 32) * 5 / 9;
}
}
const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 100; // 触发 setter
console.log(temp._celsius); // 37.777...
11.6.5 继承与 super 关键字
通过 extends 关键字可以轻松实现继承,子类可以复用父类的属性和方法。super 关键字用于访问父类的构造函数或方法。
- 调用父类构造函数:子类的
constructor中必须先调用super()才能使用this。 - 在子类方法中调用父类方法:使用
super.methodName()。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} 发出声音`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须调用 super
this.breed = breed;
}
speak() {
super.speak(); // 调用父类的 speak
console.log('汪汪!');
}
}
const dog = new Dog('旺财', '中华田园犬');
dog.speak();
// 旺财 发出声音
// 汪汪!
原型链关系:Dog.prototype 的原型是 Animal.prototype,Dog 的原型(proto)是 Animal,从而实现了完整的继承。
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Dog) === Animal); // true
11.6.6 私有属性和方法(# 前缀)
ES2022 引入了私有字段,通过在属性或方法名前加 # 声明真正的私有成员,它只能在类的内部访问,外部或子类都无法直接读写。
class Counter {
#count = 0; // 私有属性
increment() {
this.#count++;
}
get value() {
return this.#count;
}
}
const c = new Counter();
c.increment();
console.log(c.value); // 1
// console.log(c.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class
私有方法和 getter/setter 同样使用 # 前缀。注意,私有属性必须先在类体中声明,不能在 constructor 中动态创建。
11.6.7 Class 的底层本质
再次强调:class 本质上仍然是函数。
class Example {}
console.log(typeof Example); // "function"
它做的事情与手动构造函数加原型赋值是完全对应的。class 带来的优势主要是语法的清晰、强制使用 new(否则报错)、允许 super 以及未来更强的私有性支持。理解原型链始终是理解 class 行为的前提——instanceof、prototype、proto 等底层机制依然适用。
11.6.8 使用时的注意事项
- 变量提升差异:
class声明不会像函数声明那样提升,必须先定义后使用(存在暂时性死区)。 - 方法不可遍历:
class中定义的方法默认是不可枚举的(enumerable: false),与手动挂在prototype上的默认行为不同(手动添加是可枚举的)。这可以避免for...in遍历时意外取出方法。 - 使用
extends时要关注this:子类没有自己的this,需要通过super借用父类的this。 - 私有字段的转译限制:
#私有字段在旧环境无法被 polyfill 完美模拟,需要 Babel 等工具转译,且转译出的代码依赖WeakMap等特性。
掌握 class 语法后,你将能够以组织更清晰、可读性更强的方式构建对象体系,同时保持对底层原型机制的深刻理解——这种“知其然,也知其所以然”的状态,正是 JavaScript 开发者不断精进的关键。