class 的底层:ClassDefinitionEvaluation 与私有字段
class 是语法糖,但不是简单的语法糖——ClassDefinitionEvaluation 在创建原型链、注册方法、处理私有字段时做了大量 function + prototype 手写无法完全复现的事情,私有字段更是彻底脱离了原型链的范畴。
🔹 Level 1 · 你需要知道的
class 的基本结构
class Animal {
#name // 私有字段声明(必须在 class 体顶部声明)
constructor(name) {
this.#name = name // 私有字段赋值
}
speak() { // 实例方法(定义在原型上)
return `${this.#name} makes a sound`
}
static create(name) { // 静态方法(定义在类本身上)
return new Animal(name)
}
get name() { // getter(定义在原型上)
return this.#name
}
}
class Dog extends Animal {
constructor(name) {
super(name) // 必须在 this 之前调用!
this.type = 'dog'
}
speak() {
return `${this.name} barks` // 通过 getter 访问私有字段
}
}
const d = new Dog('Rex')
console.log(d.speak()) // 'Rex barks'
console.log(d instanceof Dog) // true
console.log(d instanceof Animal) // true
方法在哪里定义
| 语法 | 定义位置 | 可枚举性 |
|---|---|---|
method() {} |
ClassName.prototype 上 |
false(不可枚举) |
static method() {} |
ClassName 上 |
false |
get prop() {} |
ClassName.prototype 上 |
false |
#privateMethod() {} |
每个实例对象上 | — |
私有字段的核心特性
#field 是真正的私有,不是约定的 _field:
class Secret {
#value = 42
reveal() { return this.#value }
}
const s = new Secret()
console.log(s.reveal()) // 42
console.log(s.#value) // SyntaxError: Private field '#value' must be declared in an enclosing class
console.log(s['#value']) // undefined(这是普通属性,不是私有字段)
console.log('#value' in s) // false(普通 in 运算符看不到私有字段)
super() 为什么必须在 this 之前
子类的 this 由 super() 创建并初始化。在调用 super() 之前使用 this 会抛出 ReferenceError:
class Parent {
constructor() { this.x = 1 }
}
class Child extends Parent {
constructor() {
// console.log(this) // ReferenceError: Must call super constructor before accessing 'this'
super()
console.log(this) // Parent { x: 1 },this 已由 super() 初始化
this.y = 2
}
}
🔸 Level 2 · 它是怎么运行的
class 继承链的完整原型结构
class Animal { ... }
class Dog extends Animal { ... }
完整原型链结构:
Dog(函数对象) Animal(函数对象)
┌─────────────────┐ ┌─────────────────┐
│ [[Prototype]] │ ────────►│ (Animal 函数) │
│ (Animal 函数) │ │ │
│ prototype ──────│──┐ │ prototype ──────│──┐
└─────────────────┘ │ └─────────────────┘ │
│ │
▼ ▼
Dog.prototype │ Animal.prototype │
┌─────────────────┐ │ ┌─────────────────┐ │
│ [[Prototype]] ──│──│──────►│ [[Prototype]] ──│──│──► Object.prototype
│ constructor ────│──│──►Dog │ constructor ────│──│──► Animal
│ speak() │ │ │ speak() │ │
└─────────────────┘ │ └─────────────────┘ │
▲ │ ▲ │
│ │ │ │
new Dog() 的实例 │ new Animal() 的实例 │
┌─────────────────┐ │ ┌─────────────────┐ │
│ [[Prototype]] ──│──┘ │ [[Prototype]] ──│──┘
│ type: 'dog' │ │(实例自有属性) │
│ #name(私有) │ │ #name(私有) │
└─────────────────┘ └─────────────────┘
两条原型链:
1. 实例原型链:实例 → Dog.prototype → Animal.prototype → Object.prototype
2. 构造函数原型链:Dog → Animal → Function.prototype
(这一条让 static 方法可以继承)
ClassDefinitionEvaluation 的执行步骤
规范 15.7.14 ClassDefinitionEvaluation 是 class 声明和 class 表达式求值时调用的核心算法,简化后的步骤:
ClassDefinitionEvaluation(classBinding, classHeritage, classBody):
步骤 1:创建类环境
- 创建 ClassEnvironment(用于私有名称的作用域)
- 若有 classBinding,将类名绑定到环境中(class 表达式的名称在内部可见)
步骤 2:处理 extends(若有)
- 对 classHeritage 求值,得到 superclass
- 验证 superclass 是函数或 null
- 确定 protoParent(= superclass.prototype 或 null)
- 确定 constructorParent(= superclass)
步骤 3:创建原型对象
- proto = OrdinaryObjectCreate(protoParent)
即:proto.[[Prototype]] = protoParent(超类的 prototype)
步骤 4:初始化构造函数
- 若 classBody 中有显式 constructor:使用它
- 若无:
- 基类:使用默认 constructor() {}
- 派生类:使用默认 constructor(...args) { super(...args) }
- 使用 OrdinaryFunctionCreate 创建函数对象 F
- 设置 F.prototype = proto
- 设置 proto.constructor = F(不可枚举)
- 若有 extends:
- F.[[Prototype]] = superclass(构造函数继承)
- F.[[ConstructorKind]] = 'derived'
- 否则:
- F.[[Prototype]] = Function.prototype
- F.[[ConstructorKind]] = 'base'
步骤 5:处理类体中的各个定义
对每个 ClassElement:
- 普通方法:PropertyDefinitionEvaluation → 定义在 proto 上(enumerable: false)
- static 方法:PropertyDefinitionEvaluation → 定义在 F 上(enumerable: false)
- 私有方法:注册到 PrivateEnvironment
- 实例字段(含私有字段):收集到 instancePrivateMethods 和 fields 列表
- static 字段:立即求值并定义到 F 上
步骤 6:安装私有方法
- 对每个私有方法,将 PrivateName 和方法关联存入 PrivateEnvironment
步骤 7:初始化 static 字段
- 按声明顺序依次求值 static 字段的初始化器
步骤 8:返回构造函数 F
私有字段的实现机制
私有字段不是原型属性,也不是普通属性,而是通过 PrivateName + [[PrivateElements]] 机制实现的:
私有字段的实现结构:
ClassEnvironment(在 class 求值时创建):
┌────────────────────────────────────────────┐
│ PrivateEnvironmentRecord │
│ #name → PrivateName { [[Description]]: '#name' } │
│ #count → PrivateName { [[Description]]: '#count' } │
└────────────────────────────────────────────┘
│
│ 每个实例创建时(new ClassName())
▼
实例对象(OrdinaryObject):
┌────────────────────────────────────────────┐
│ [[PrivateElements]] │
│ [ { [[Key]]: PrivateName(#name), │
│ [[Kind]]: 'field', │
│ [[Value]]: 'Rex' }, │
│ { [[Key]]: PrivateName(#count), │
│ [[Kind]]: 'field', │
│ [[Value]]: 0 } ] │
└────────────────────────────────────────────┘
访问 obj.#name 时:
1. 在 obj.[[PrivateElements]] 中查找 Key === PrivateName(#name) 的条目
2. 若找到:返回 [[Value]]
3. 若未找到:抛出 TypeError(不是 SyntaxError!)
"Cannot read private member #name from an object whose class did not declare it"
class Dog {
#name
constructor(name) { this.#name = name }
static isInstance(obj) {
return #name in obj // 通过 in 运算符检测私有字段(ES2022)
}
}
class Cat {
#name
constructor(name) { this.#name = name }
}
const dog = new Dog('Rex')
const cat = new Cat('Whiskers')
console.log(Dog.isInstance(dog)) // true
console.log(Dog.isInstance(cat)) // false(Cat 的 #name 和 Dog 的 #name 是不同的 PrivateName)
私有方法的存储位置
私有方法与公共方法不同——公共方法定义在原型上(所有实例共享),私有方法的方法对象存储在每个实例的 [[PrivateElements]] 中(但函数对象本身是共享的):
公共方法 vs 私有方法的存储差异:
公共方法 speak():
Dog.prototype.speak = function() { ... }
所有实例通过原型链共享同一个函数对象
内存开销:O(1),与实例数量无关
私有方法 #speak():
每个实例的 [[PrivateElements]] 中都有一条记录:
{ [[Key]]: PrivateName(#speak), [[Kind]]: 'method', [[Value]]: <function> }
虽然 [[Value]] 指向同一个函数对象,但每个实例都有自己的 PrivateElement 条目
内存开销:O(n),与实例数量成正比(每条记录有额外开销)
class WithPrivateMethod {
#secret() { return 42 }
publicCall() { return this.#secret() }
}
// 私有方法不可从外部访问
const obj = new WithPrivateMethod()
console.log(obj.publicCall()) // 42
console.log(obj['#secret']) // undefined(不是这样访问的)
// obj.#secret() // SyntaxError(#secret 不在当前词法范围内)
class 声明的 TDZ
class 声明和 let/const 一样存在 TDZ(Temporal Dead Zone)——声明会提升,但在执行到声明语句之前不可访问:
class 声明的提升与 TDZ:
执行前:
┌──────────────────────────────────────┐
│ 变量环境 │
│ Animal → <uninitialized>(TDZ) │
│ Dog → <uninitialized>(TDZ) │
└──────────────────────────────────────┘
执行到 class Animal { ... } 时:
┌──────────────────────────────────────┐
│ 变量环境 │
│ Animal → [Animal 函数对象] │
│ Dog → <uninitialized>(TDZ) │
└──────────────────────────────────────┘
// class 声明有 TDZ,不能在声明前使用
new MyClass() // ReferenceError: Cannot access 'MyClass' before initialization
class MyClass {}
// function 声明完全提升,可以在声明前使用
new MyFunc() // 正常工作
function MyFunc() {}
🔺 Level 3 · 规范怎么定义的
15.7 Class Definitions
规范 15.7 定义了 class 相关语法的求值。Class 声明的运行时语义引用 15.7.14 ClassDefinitionEvaluation,以下是关键的算法节点:
ClassElement 的分类(15.7.1):
ClassElement 类型:
MethodDefinition → 实例方法
static MethodDefinition → 静态方法
FieldDefinition ; → 实例字段
static FieldDefinition ; → 静态字段
ClassStaticBlock → 静态初始化块(ES2022)
; → 空元素(忽略)
类字段初始化的时机:
实例字段(#field = expr、field = expr)的初始化不在 ClassDefinitionEvaluation 中执行,而是被收集为初始化器(InitializeInstanceElements),在每次 new 创建实例时([[Construct]] 步骤中)执行:
[[Construct]] 中与 class 相关的额外步骤:
1. 创建新对象 thisArgument
2. 调用 InitializeInstanceElements(thisArgument, F):
a. 安装私有方法:将 F.[[PrivateMethods]] 里的每个私有方法
添加到 thisArgument.[[PrivateElements]]
b. 执行字段初始化器:按声明顺序对每个字段执行初始化器
- 私有字段 #f = expr → 将 PrivateName → value 添加到 [[PrivateElements]]
- 公共字段 f = expr → DefinePropertyOrThrow(thisArgument, 'f', ...)
3. 执行构造函数体
PrivateEnvironment Records(规范 9.2)
PrivateEnvironmentRecord 是一种新的环境记录类型(ES2022 引入),专门用于管理私有名称的作用域:
PrivateEnvironmentRecord 的结构:
{
[[OuterPrivateEnvironment]]: 外层 PrivateEnvironmentRecord 或 null,
[[Names]]: 私有名称列表(PrivateName 对象)
}
PrivateName 对象:
{
[[Description]]: '#fieldName'(用于错误信息)
}
注意:PrivateName 是对象引用,===/=== 比较检查引用相等性
因此 Dog 的 #name 和 Cat 的 #name 是完全不同的两个 PrivateName 对象
即使描述字符串相同,它们也不相等(这保证了跨类访问的安全性)
[[PrivateElements]] 的存储与访问
每个对象实例的 [[PrivateElements]] 是一个 List,每个条目是 PrivateElement 记录:
PrivateElement Record:
{
[[Key]]: PrivateName(引用,不是字符串)
[[Kind]]: 'field' | 'method' | 'accessor'
[[Value]]: 字段值 / 方法函数对象
(accessor 类型有 [[Get]] 和 [[Set]] 代替 [[Value]])
}
访问私有字段的操作 PrivateGet(P, O)(规范 7.3.26):
PrivateGet(P, O):
// P 是 PrivateName,O 是对象
1. 令 entry = PrivateElementFind(O, P)
// 在 O.[[PrivateElements]] 中线性查找 [[Key]] === P 的条目
2. 若 entry 是 undefined:
抛出 TypeError("Cannot read private member of an object
whose class did not declare it")
3. 若 entry.[[Kind]] === 'field':
返回 entry.[[Value]]
4. 若 entry.[[Kind]] === 'method':
返回 entry.[[Value]](直接返回方法函数)
5. 若 entry.[[Kind]] === 'accessor':
若 entry.[[Get]] 是 undefined:抛出 TypeError
返回 Call(entry.[[Get]], O)
写入私有字段 PrivateSet(P, O, value)(规范 7.3.27):
PrivateSet(P, O, value):
1. 令 entry = PrivateElementFind(O, P)
2. 若 entry 是 undefined:抛出 TypeError
3. 若 entry.[[Kind]] === 'field':
entry.[[Value]] = value
4. 若 entry.[[Kind]] === 'method':
抛出 TypeError(私有方法不能被赋值)
5. 若 entry.[[Kind]] === 'accessor':
若 entry.[[Set]] 是 undefined:抛出 TypeError
Call(entry.[[Set]], O, [value])
私有字段的 in 运算符(规范 13.10.1)
ES2022 引入了私有字段的 in 检测(Ergonomic brand checks):
HasPrivateName(O, P):
// 规范中 #field in obj 的实现
1. 令 entry = PrivateElementFind(O, P)
2. 若 entry 不是 undefined:返回 true
3. 返回 false
这个操作比 instanceof 更精确:instanceof 检查原型链,在多 Realm 或修改了 Symbol.hasInstance 时可能失效;#field in obj 直接检查对象的 [[PrivateElements]],不受原型链影响。
💎 Level 4 · 边界与陷阱
陷阱 1:子类 constructor 的 this 初始化规则
子类([[ConstructorKind]] === 'derived')的 this 由父类 [[Construct]] 创建,不是由子类创建。在 super() 返回之前,this 处于"未初始化"状态:
class Base {
constructor() {
this.baseField = 1
}
}
class Derived extends Base {
#privateField
constructor() {
// 在这里访问 this 会抛出 ReferenceError
// console.log(this) // ReferenceError
super()
// super() 返回后,this 已经被 Base 构造函数初始化
// 并且 InitializeInstanceElements 已经安装了私有字段
console.log(this.baseField) // 1
this.#privateField = 42 // 现在可以操作私有字段
}
}
new Derived()
子类如果完全省略 constructor,会使用默认的 constructor(...args) { super(...args) },自动转发所有参数。
陷阱 2:静态私有字段只有该类本身能访问,子类也不行
class Parent {
static #secret = 'parent secret'
static getSecret() {
return Parent.#secret // 正确:在 Parent 类的词法范围内
}
}
class Child extends Parent {
static getChildSecret() {
// return Child.#secret // SyntaxError:#secret 没有在 Child 中声明
// 静态私有字段不会继承!
return Parent.getSecret() // 只能通过父类的公共方法间接访问
}
}
console.log(Parent.getSecret()) // 'parent secret'
console.log(Child.getChildSecret()) // 'parent secret'
// console.log(Child.#secret) // SyntaxError
陷阱 3:#field in obj 是比 instanceof 更可靠的实例检测
class MyClass {
#brand // 私有字段用于品牌检测
constructor() {
this.#brand = true
}
static isInstance(obj) {
return #brand in obj // 只要对象有这个私有字段,就是 MyClass 的实例
}
}
// instanceof 的问题:可以被篡改
class FakeClass {
static [Symbol.hasInstance](obj) { return true } // 伪造 instanceof
}
console.log({} instanceof FakeClass) // true!假阳性
// 跨 Realm 时 instanceof 失效
// 例如 iframe 中的数组
// [] instanceof Array // false(不同 Realm 的 Array 不是同一个构造函数)
// #brand in obj 的优势:
const obj = new MyClass()
console.log(MyClass.isInstance(obj)) // true
console.log(MyClass.isInstance({})) // false
console.log(MyClass.isInstance(null)) // false(不抛出,返回 false)
陷阱 4:class 方法不可枚举,普通对象字面量方法可枚举
这个差异在序列化、for...in 遍历时容易踩坑:
class Foo {
bar() {}
}
const obj = {
bar() {}
}
// 检查可枚举性
const classDescriptor = Object.getOwnPropertyDescriptor(Foo.prototype, 'bar')
console.log(classDescriptor.enumerable) // false
const objDescriptor = Object.getOwnPropertyDescriptor(obj, 'bar')
console.log(objDescriptor.enumerable) // true
// 实际影响:for...in 不遍历 class 方法
for (const key in new Foo()) {
console.log(key) // 没有输出(bar 不可枚举)
}
for (const key in obj) {
console.log(key) // 'bar'(可枚举)
}
// JSON.stringify 不会序列化方法,但这里注意的是原型属性
// class 方法在原型上,JSON.stringify 不管可枚举性,方法本来就不能序列化
陷阱 5:私有方法的内存开销是公共方法的 n 倍
class PublicMethod {
method() { return 42 }
}
class PrivateMethod {
#method() { return 42 }
call() { return this.#method() }
}
// 创建 10000 个实例
const publicInstances = Array.from({ length: 10000 }, () => new PublicMethod())
const privateInstances = Array.from({ length: 10000 }, () => new PrivateMethod())
// 公共方法:所有实例共享 PublicMethod.prototype.method,只有 1 个函数对象
// 私有方法:每个实例的 [[PrivateElements]] 中都有一个 PrivateElement 条目
// 虽然 [[Value]] 指向同一个函数对象,但 10000 个条目本身有内存开销
// 在内存敏感的场景下,私有方法应谨慎使用
// 可以改用 WeakMap 模拟私有性(旧方法)或直接用公共方法(若安全性允许)
// WeakMap 模式(私有性但不额外占用每实例内存)
const _method = new WeakMap()
class WeakMapPrivate {
constructor() {
_method.set(this, function() { return 42 })
}
call() { return _method.get(this)() }
}
本章小结
ClassDefinitionEvaluation创建两条原型链:实例链(实例→ClassName.prototype → ParentClass.prototype)和构造函数链(ClassName → ParentClass),后者使静态方法可以继承。- 私有字段通过 PrivateName + [[PrivateElements]] 实现,PrivateName 是对象引用(不是字符串),跨类的同名私有字段是完全不同的两个 PrivateName,无法互相访问。
- 子类的
this由super()调用父类[[Construct]]创建,super()之前访问this抛出ReferenceError;字段初始化器(含私有字段)在super()之后、构造函数体继续之前执行。 - 静态私有字段(
static #field)只在声明它的类的词法范围内可见,子类无法继承或访问,连通过实例也不行。 - 私有方法的每个实例都在
[[PrivateElements]]中保有独立条目,内存开销与实例数量成正比(O(n)),在高频创建实例的场景中应评估影响。