第 23 章

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 之前

子类的 thissuper() 创建并初始化。在调用 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 = exprfield = 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:子类 constructorthis 初始化规则

子类([[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)() }
}

本章小结

  1. ClassDefinitionEvaluation 创建两条原型链:实例链(实例→ClassName.prototype → ParentClass.prototype)和构造函数链(ClassName → ParentClass),后者使静态方法可以继承。
  2. 私有字段通过 PrivateName + [[PrivateElements]] 实现,PrivateName 是对象引用(不是字符串),跨类的同名私有字段是完全不同的两个 PrivateName,无法互相访问。
  3. 子类的 thissuper() 调用父类 [[Construct]] 创建,super() 之前访问 this 抛出 ReferenceError;字段初始化器(含私有字段)在 super() 之后、构造函数体继续之前执行。
  4. 静态私有字段(static #field)只在声明它的类的词法范围内可见,子类无法继承或访问,连通过实例也不行。
  5. 私有方法的每个实例都在 [[PrivateElements]] 中保有独立条目,内存开销与实例数量成正比(O(n)),在高频创建实例的场景中应评估影响。
本章评分
4.8  / 5  (6 评分)

💬 留言讨论