JavaScript class 类语法入门
概述
class 是 ES6 提供的定义类的语法。它本质上是原型继承的语法糖——typeof MyClass 返回的是 'function',方法最终还是挂在 prototype 上。但它不只是「换个写法」,在语义上有若干和 function 构造器不同的强约束。
在 class 之前我们这样写「类」:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log('hi, ' + this.name);
};
问题是构造逻辑和方法定义分散在两处,继承要手动处理 prototype 链和 constructor 指向,代码噪音很大。class 把这些收拢到一个块里:
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hi, ${this.name}`);
}
}
const p = new Person('wfly');
p.sayHi(); // hi, wfly
console.log(typeof Person); // 'function'
console.log(Object.getOwnPropertyNames(Person.prototype)); // ['constructor', 'sayHi']
sayHi 定义在 Person.prototype 上,所有实例共享同一个函数对象,不会每次 new 都创建一份。
与 function 构造器的关键差异
这几条是实际写代码会撞到的:
必须用 new 调用。直接调用 Person('x') 会抛 TypeError: Class constructor Person cannot be invoked without 'new'。function 构造器直接调用不会报错,只会让 this 指向 undefined 或全局对象,静默产生 bug。
不会被提升。class 声明存在于暂时性死区(TDZ),在声明之前访问会抛 ReferenceError,而 function 声明会被提升可以提前调用。
内部代码默认是严格模式,不需要写 'use strict'。
原型上的方法不可枚举。for...in 遍历实例时不会遍历到 sayHi,而 Person.prototype.sayHi = ... 这种手写方式定义的方法是可枚举的。
实例字段与静态成员
类字段(class fields)可以直接写在类体里,等价于在 constructor 里赋值给 this:
class Counter {
count = 0; // 实例字段,每个实例一份
static total = 0; // 静态字段,挂在类本身上
static create() {
Counter.total++;
return new Counter();
}
inc = () => {
// 箭头函数字段,this 永久绑定实例
this.count++;
};
dec() {
// 原型方法,this 取决于调用方式
this.count--;
}
}
const c = Counter.create();
console.log(Counter.total); // 1
const { inc, dec } = c;
inc(); // 正常,count 变 1
dec(); // TypeError: Cannot read properties of undefined
这里的差别很关键:inc 是实例字段,值是一个箭头函数,箭头函数捕获定义时的 this(也就是实例),所以脱离对象调用也没问题;dec 是原型方法,解构出来之后 this 丢了。React 里写 onClick={this.handleClick} 需要 bind 或者写成箭头函数字段,原因就在这。
代价是箭头函数字段每个实例都存一份,不共享,实例多时内存开销更大。
私有字段
以 # 开头的是真正的私有字段,从类外部访问是语法错误(不是运行时报 undefined):
class Account {
#balance = 0;
deposit(n) {
if (n <= 0) throw new Error('金额必须为正');
this.#balance += n;
return this;
}
get balance() {
return this.#balance;
}
// 静态方法里可以访问同类实例的私有字段
static isAccount(obj) {
return #balance in obj; // ES2022 的 in 检测,不会抛错
}
}
const a = new Account();
a.deposit(100);
console.log(a.balance); // 100
// console.log(a.#balance); // SyntaxError
这比早年用 _balance 加下划线约定或者用闭包模拟私有要彻底得多。注意私有字段不会出现在 Object.keys、JSON.stringify 的结果里,所以带私有字段的实例序列化后会丢这部分数据。
继承与 super
用 extends 继承,子类的 constructor 里必须先调用 super() 才能用 this:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} 发出声音`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须在使用 this 之前调用
this.breed = breed;
}
speak() {
return `${super.speak()}:汪`; // super 调用父类方法
}
}
const d = new Dog('旺财', '柴犬');
console.log(d.speak()); // 旺财 发出声音:汪
console.log(d instanceof Animal); // true
super() 之前访问 this 会抛 ReferenceError,因为在 ES6 的语义里,子类的 this 是由父类构造函数创建后再返回给子类的,super() 没执行 this 就还不存在。这和 function 时代先创建 this 再修饰的顺序正好相反。
如果子类完全不写 constructor,引擎会自动补一个 constructor(...args) { super(...args); }。
getter / setter
get 和 set 定义访问器属性,配合私有字段能做校验:
class Temperature {
#celsius = 0;
get fahrenheit() {
return this.#celsius * 1.8 + 32;
}
set fahrenheit(v) {
if (typeof v !== 'number' || Number.isNaN(v)) {
throw new TypeError('必须是数字');
}
this.#celsius = (v - 32) / 1.8;
}
}
const t = new Temperature();
t.fahrenheit = 212;
console.log(t.fahrenheit); // 212
访问器定义在 prototype 上,用起来像普通属性但走的是函数调用。要注意 getter 里不要写重计算逻辑,因为调用方看着像读属性,很容易在循环里反复读导致性能问题。
一个容易忽略的坑
class 里的方法不能用 new 调用。class 的原型方法没有 [[Construct]] 内部方法,new p.sayHi() 会抛 TypeError: p.sayHi is not a constructor。同样它们也没有 prototype 属性。这在把老代码里 Foo.prototype.Bar = function(){} 这种「在原型上挂构造器」的写法迁移到 class 时会直接失败,得改成静态字段或者独立的类。

还没有留言,来发表第一条吧~