第 12 章

属性描述符:4个内部槽与 Object.defineProperty

每个 JavaScript 属性背后都不只有一个值——它还携带着4个控制位,决定这个属性能否被写入、枚举、重新配置,以及删除。理解这4个内部槽,是读懂 Vue 2 响应式、Object.freeze、以及各种诡异赋值失效问题的根本前提。

🔹 Level 1 · 你需要知道的

4个属性描述符

JavaScript 每个数据属性(data property)都有4个内部槽:

描述符 类型 默认值(直接赋值) 默认值(defineProperty) 含义
value 任意 赋值的值 undefined 属性的当前值
writable boolean true false 是否允许修改 value
enumerable boolean true false 是否出现在 for...in / Object.keys
configurable boolean true false 是否允许删除或重新定义

关键规律:用 obj.x = 1 这种普通赋值创建的属性,三个控制位全部为 true;用 Object.defineProperty 创建的属性,未指定的控制位全部默认为 false

查看描述符

const obj = { x: 1 };
Object.getOwnPropertyDescriptor(obj, 'x');
// { value: 1, writable: true, enumerable: true, configurable: true }

const obj2 = {};
Object.defineProperty(obj2, 'x', { value: 1 });
Object.getOwnPropertyDescriptor(obj2, 'x');
// { value: 1, writable: false, enumerable: false, configurable: false }
// 查看所有属性的描述符
Object.getOwnPropertyDescriptors(obj);

Object.defineProperty 的使用场景

场景1:Vue 2 响应式原理

Vue 2 通过 Object.defineProperty 将普通数据属性改写为访问器属性,劫持读写操作:

function defineReactive(obj, key, val) {
  const dep = new Dep(); // 依赖收集器

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend(); // 收集当前正在计算的 watcher
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify(); // 通知所有依赖更新
    }
  });
}

const data = { message: 'hello' };
defineReactive(data, 'message', data.message);
// 现在 data.message 的读写都被拦截了

场景2:创建只读常量

const config = {};
Object.defineProperty(config, 'API_URL', {
  value: 'https://api.example.com',
  writable: false,
  enumerable: true,
  configurable: false
});

config.API_URL = 'http://hack.com'; // 静默失败(非严格模式)或 TypeError(严格模式)
console.log(config.API_URL); // 'https://api.example.com'

场景3:隐藏内部属性

class EventEmitter {
  constructor() {
    Object.defineProperty(this, '_listeners', {
      value: new Map(),
      writable: false,
      enumerable: false, // 不出现在 for...in 和 JSON.stringify
      configurable: false
    });
  }
}

const emitter = new EventEmitter();
console.log(Object.keys(emitter)); // [] — _listeners 被隐藏了
JSON.stringify(emitter); // '{}'

Object.freeze vs Object.seal vs Object.preventExtensions

这三个方法都是"冻结"对象的不同程度,经常被混淆:

方法 不可添加属性 不可删除属性 不可修改值 描述符变化
Object.preventExtensions
Object.seal configurable → false
Object.freeze configurable+writable → false
// preventExtensions:不能加新属性,但能改现有属性
const a = { x: 1 };
Object.preventExtensions(a);
a.x = 99;    // 可以
a.y = 2;     // 静默失败(严格模式报错)
delete a.x;  // 可以

// seal:不能加也不能删,但能改值
const b = { x: 1 };
Object.seal(b);
b.x = 99;    // 可以
b.y = 2;     // 失败
delete b.x;  // 失败

// freeze:完全不可变(但是浅冻结!)
const c = { x: 1, nested: { y: 2 } };
Object.freeze(c);
c.x = 99;          // 失败
c.nested.y = 99;   // 成功!— freeze 不递归

深度冻结需要自行递归:

function deepFreeze(obj) {
  Object.getOwnPropertyNames(obj).forEach(name => {
    const value = obj[name];
    if (typeof value === 'object' && value !== null) {
      deepFreeze(value);
    }
  });
  return Object.freeze(obj);
}

5个常见错误

错误1:以为 const 能防止属性修改

const obj = { x: 1 };
obj.x = 99; // 完全可以!const 只防止重新赋值变量本身
obj = {};   // 这才报错:Assignment to constant variable

错误2:Object.defineProperty 的默认值陷阱

// 以为只设置 value,其他保持"现有值"
const obj = { x: 1 };
Object.defineProperty(obj, 'x', { value: 2 }); // writable 变成 false!
obj.x = 3; // 静默失败
// 应该写:
Object.defineProperty(obj, 'x', { value: 2, writable: true, configurable: true, enumerable: true });

错误3:freeze 后以为数组不可变

const arr = [1, 2, 3];
Object.freeze(arr);
arr.push(4); // TypeError: Cannot add property 3, object is not extensible
arr[0] = 99; // TypeError: Cannot assign to read only property '0'
// 但是:
const arr2 = Object.freeze([{ val: 1 }]);
arr2[0].val = 99; // 成功!元素是对象,没被冻结

错误4:for...in 遍历了不期望的属性

function Animal(name) { this.name = name; }
Animal.prototype.breathe = function() {};

const dog = new Animal('Rex');
for (const key in dog) {
  console.log(key); // 'name' 和 'breathe' — 原型上的属性也被枚举了
}

// 正确做法:用 hasOwnProperty 过滤,或用 for...of + Object.keys
for (const key in dog) {
  if (Object.prototype.hasOwnProperty.call(dog, key)) {
    console.log(key); // 只有 'name'
  }
}

错误5:误以为 writable:false 能阻止原型上的属性被遮蔽

// 这个在 Level 4 详细讨论,但先记住结论:
// 原型上 writable:false 的属性,在子对象上直接赋值不会创建自有属性
// 也不会报错(非严格模式),这是最隐蔽的陷阱之一

🔸 Level 2 · 它是怎么运行的

内部槽的实际存储结构

每个属性在引擎内部是一个 Property Descriptor Record,包含不同字段的组合。关键区分:数据属性访问器属性在内部结构上完全不同,且同一属性不能同时是两种类型

数据属性内部槽:
┌─────────────────────────────────────────────────────────┐
│  Property Descriptor (Data)                             │
│                                                         │
│  [[Value]]        : 任意 ECMAScript 值                  │
│  [[Writable]]     : Boolean(能否修改 [[Value]])        │
│  [[Enumerable]]   : Boolean(能否被枚举)                │
│  [[Configurable]] : Boolean(能否被重新配置/删除)        │
└─────────────────────────────────────────────────────────┘

访问器属性内部槽:
┌─────────────────────────────────────────────────────────┐
│  Property Descriptor (Accessor)                         │
│                                                         │
│  [[Get]]          : Function | undefined(读取调用)     │
│  [[Set]]          : Function | undefined(写入调用)     │
│  [[Enumerable]]   : Boolean                             │
│  [[Configurable]] : Boolean                             │
└─────────────────────────────────────────────────────────┘

注意:数据属性有 [[Value]][[Writable]],访问器属性有 [[Get]][[Set]]。两者共享 [[Enumerable]][[Configurable]],但前面两个字段互斥。

普通赋值 vs defineProperty 的执行路径

当你写 obj.x = 1 时,引擎走的是 [[Set]] 内部方法;当你调用 Object.defineProperty 时,走的是 [[DefineOwnProperty]]。这两条路径的行为在很多边界情况下不同。

obj.x = 1 的执行路径:
┌──────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│  obj.[[Set]]('x', 1, obj)                                               │
│       │                                                                  │
│       ▼                                                                  │
│  1. 检查 obj 是否有自有属性 'x'                                           │
│       │                                                                  │
│       ├── 有,且是数据属性                                                │
│       │    ├── writable: false → 严格模式 TypeError,非严格静默失败        │
│       │    └── writable: true → 调用 [[DefineOwnProperty]] 仅更新 value  │
│       │                                                                  │
│       ├── 有,且是访问器属性                                              │
│       │    ├── [[Set]] 为 undefined → TypeError                         │
│       │    └── [[Set]] 存在 → 调用 setter 函数                           │
│       │                                                                  │
│       └── 没有 → 沿原型链查找(见 ch13)                                  │
│            └── 最终:在 obj 上创建新数据属性(writable/enumerable/        │
│                configurable 全为 true)                                  │
└──────────────────────────────────────────────────────────────────────────┘
Object.defineProperty(obj, 'x', desc) 的执行路径:
┌──────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│  ValidateAndApplyPropertyDescriptor(obj, 'x', true, desc, current)      │
│       │                                                                  │
│       ▼                                                                  │
│  1. current 是 undefined(属性不存在)                                    │
│       └── obj 是否可扩展?                                               │
│            ├── 否 → TypeError                                            │
│            └── 是 → 创建属性,使用 desc 指定的值,未指定字段用默认值       │
│                                                                          │
│  2. current 存在                                                         │
│       └── 调用合法性检查(详见 Level 3)                                  │
│            ├── 合法 → 更新指定字段                                        │
│            └── 不合法 → TypeError                                        │
└──────────────────────────────────────────────────────────────────────────┘

configurable: false 之后允许的变更

这是最容易搞错的点。configurable: false 并不是"完全不能改":

configurable: false 时的规则表:
┌──────────────────────────────────────────────────────┬────────┐
│ 操作                                                 │ 是否允许│
├──────────────────────────────────────────────────────┼────────┤
│ 删除属性(delete)                                    │   ✗    │
│ 将数据属性改为访问器属性                               │   ✗    │
│ 将访问器属性改为数据属性                               │   ✗    │
│ 修改 [[Enumerable]]                                  │   ✗    │
│ 修改 [[Configurable]](从 false 改为 true)           │   ✗    │
│ 修改 [[Value]](当 writable:true 时)                 │   ✓    │
│ 将 [[Writable]] 从 true 改为 false                   │   ✓    │
│ 将 [[Writable]] 从 false 改为 true                   │   ✗    │
│ 修改访问器的 [[Get]] 或 [[Set]]                       │   ✗    │
└──────────────────────────────────────────────────────┴────────┘

这个规则的设计逻辑是:收紧总是允许的,放开是不允许的。writable 从 true 到 false 是收紧(相当于"自愿放弃写权限"),反向是放开。

const obj = {};
Object.defineProperty(obj, 'x', {
  value: 1,
  writable: true,
  configurable: false
});

// 允许:把 writable 从 true 改为 false
Object.defineProperty(obj, 'x', { writable: false }); // OK

// 不允许:把 writable 从 false 改为 true
Object.defineProperty(obj, 'x', { writable: true }); // TypeError

// 不允许:修改 enumerable
Object.defineProperty(obj, 'x', { enumerable: true }); // TypeError

// 如果 writable 还是 true,允许修改 value
const obj2 = {};
Object.defineProperty(obj2, 'x', { value: 1, writable: true, configurable: false });
Object.defineProperty(obj2, 'x', { value: 2 }); // OK,value 从 1 改为 2

访问器属性的完整机制

const obj = {};
let _x = 0;

Object.defineProperty(obj, 'x', {
  get() {
    console.log('reading x');
    return _x;
  },
  set(val) {
    console.log(`writing x: ${val}`);
    _x = val;
  },
  enumerable: true,
  configurable: true
});

// 简写语法(等价):
const obj2 = {
  get x() { return _x; },
  set x(val) { _x = val; }
};

访问器属性的典型模式:计算属性、懒初始化、验证:

class Temperature {
  #celsius = 0;

  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32;
  }

  set fahrenheit(f) {
    if (typeof f !== 'number') throw new TypeError('Temperature must be a number');
    this.#celsius = (f - 32) * 5 / 9;
  }

  get celsius() { return this.#celsius; }
  set celsius(c) {
    if (c < -273.15) throw new RangeError('Below absolute zero');
    this.#celsius = c;
  }
}

const t = new Temperature();
t.celsius = 100;
console.log(t.fahrenheit); // 212
t.fahrenheit = 32;
console.log(t.celsius);    // 0

🔺 Level 3 · 规范怎么定义的

规范 6.2.6:Property Descriptor 类型

ECMAScript 规范(ECMA-262)在 §6.2.6 定义了 Property Descriptor 类型,它是规范层面的 Record 类型(不是 JavaScript 对象,是引擎内部的抽象概念)。

规范原文(§6.2.6.1 IsAccessorDescriptor):

The abstract operation IsAccessorDescriptor takes argument Desc (a Property Descriptor or undefined) and returns a Boolean. It performs the following steps when called:

  1. If Desc is undefined, return false.
  2. If Desc has a [[Get]] field, return true.
  3. If Desc has a [[Set]] field, return true.
  4. Return false.

类似地,IsDataDescriptor 检查 [[Value]] 或 [[Writable]] 字段,IsGenericDescriptor 用于两者都不是的情况(只有 enumerable/configurable 的描述符)。

[[DefineOwnProperty]] 的合法性检查算法

ValidateAndApplyPropertyDescriptor 是整个属性描述符系统的核心。以下是规范算法(§10.1.6.3)的中文逐步解读:

输入:O(对象或 undefined),P(属性键),extensible(布尔),Desc(新描述符),current(当前描述符或 undefined)

步骤解读

Step 1-2: 如果 current 是 undefined(属性不存在)
  如果 extensible 是 false → 返回 false(不能添加属性)
  否则:
    如果 Desc 是 IsGenericDescriptor 或 IsDataDescriptor
      → 创建数据属性,[[Value]]、[[Writable]]、[[Enumerable]]、[[Configurable]]
        用 Desc 中的值,未指定的用 false/undefined
    否则(Desc 是访问器描述符)
      → 创建访问器属性,[[Get]]、[[Set]]、[[Enumerable]]、[[Configurable]]
  → 返回 true

Step 3: 如果 Desc 的所有字段都和 current 相同 → 返回 true(无变化)

Step 4: 如果 current.[[Configurable]] 是 false:
  4a. Desc.[[Configurable]] 是 true → 返回 false(不能改为 configurable)
  4b. Desc.[[Enumerable]] 存在且与 current.[[Enumerable]] 不同 → 返回 false

Step 5: 如果 IsGenericDescriptor(Desc) → 跳到 Step 8(只更新 configurable/enumerable)

Step 6: 如果 IsDataDescriptor(current) ≠ IsDataDescriptor(Desc)(类型转换)
  如果 current.[[Configurable]] 是 false → 返回 false
  否则:转换属性类型(数据 ↔ 访问器),保留 configurable/enumerable

Step 7: 如果两者都是数据属性:
  如果 current.[[Configurable]] 是 false 且 current.[[Writable]] 是 false:
    7a. Desc.[[Writable]] 是 true → 返回 false
    7b. Desc.[[Value]] 存在且 SameValue(Desc.[[Value]], current.[[Value]]) 是 false → 返回 false

Step 8: 如果两者都是访问器属性:
  如果 current.[[Configurable]] 是 false:
    Desc.[[Set]] 存在且与 current.[[Set]] 不同 → 返回 false
    Desc.[[Get]] 存在且与 current.[[Get]] 不同 → 返回 false

Step 9: 如果 O 不是 undefined(真实对象操作,不是仅验证)
  → 将 Desc 中存在的字段更新到属性

Step 10: 返回 true

注意:Step 7b 用的是 SameValue(即 Object.is),不是 ===。这意味着 NaNNaN 是相同的,+0-0 不同。

Object.defineProperty 的规范实现

规范 §20.1.2.4 Object.defineProperty(O, P, Attributes)

1. 如果 O 不是对象 → 抛出 TypeError
2. key ← ToPropertyKey(P)
3. desc ← ToPropertyDescriptor(Attributes)
   (将普通 JS 对象转换为内部 Property Descriptor Record)
4. 调用 O.[[DefineOwnProperty]](key, desc)
5. 如果结果是 false → 抛出 TypeError
6. 返回 O

ToPropertyDescriptor(Obj) 的关键步骤(§6.2.6.6):

// 规范算法的 JS 伪代码表示
function ToPropertyDescriptor(Obj) {
  if (typeof Obj !== 'object') throw new TypeError();

  const desc = {};

  if ('enumerable' in Obj) desc.enumerable = Boolean(Obj.enumerable);
  if ('configurable' in Obj) desc.configurable = Boolean(Obj.configurable);
  if ('value' in Obj) desc.value = Obj.value;
  if ('writable' in Obj) desc.writable = Boolean(Obj.writable);
  if ('get' in Obj) {
    if (typeof Obj.get !== 'function' && Obj.get !== undefined)
      throw new TypeError();
    desc.get = Obj.get;
  }
  if ('set' in Obj) {
    if (typeof Obj.set !== 'function' && Obj.set !== undefined)
      throw new TypeError();
    desc.set = Obj.set;
  }

  // 不变量检查
  if ('get' in desc || 'set' in desc) {
    if ('value' in desc || 'writable' in desc)
      throw new TypeError('accessor and data descriptor are mutually exclusive');
  }

  return desc;
}

💎 Level 4 · 边界与陷阱

陷阱1:Object.freeze 之后 Array.push 为何报 TypeError

这是最常见的"我以为我懂 freeze"的场景:

const arr = [1, 2, 3];
Object.freeze(arr);
arr.push(4); // TypeError: Cannot add property 3, object is not extensible

完整推导

Array.prototype.push 的规范实现(§23.1.3.20)等价于:

Array.prototype.push = function(...items) {
  let len = this.length; // 读取 length
  for (const item of items) {
    this[len] = item;     // 1. 设置索引属性
    len++;
  }
  this.length = len;      // 2. 更新 length
  return len;
};

Object.freeze 对数组做了什么:

Object.freeze(arr);
// 等价于:
Object.preventExtensions(arr); // 不能添加新属性(索引 3 不存在)
// 对每个属性:
Object.defineProperty(arr, '0', { writable: false, configurable: false });
Object.defineProperty(arr, '1', { writable: false, configurable: false });
Object.defineProperty(arr, '2', { writable: false, configurable: false });
Object.defineProperty(arr, 'length', { writable: false }); // length 也变为不可写!

push 操作中:

// 验证 length 的描述符:
Object.getOwnPropertyDescriptor(arr, 'length');
// { value: 3, writable: false, enumerable: false, configurable: false }

真实 bug 案例:Redux store 使用 Object.freeze 防止直接修改 state,开发者在 reducer 中误用 state.list.push(item) 而不是 [...state.list, item],导致严格模式下测试通过(TypeError 被测试框架捕获)但生产环境静默失败(修改了 state 但 UI 不更新),因为生产环境用了不同的 freeze 策略。

陷阱2:configurable:false 后 writable 单向可改

const obj = {};
Object.defineProperty(obj, 'x', {
  value: 42,
  writable: true,
  configurable: false,
  enumerable: true
});

// 第一步:writable true → false(允许,属于"收紧")
Object.defineProperty(obj, 'x', { writable: false });
// 现在 x 的描述符:{ value: 42, writable: false, configurable: false, enumerable: true }

// 第二步:尝试 writable false → true(不允许)
Object.defineProperty(obj, 'x', { writable: true });
// TypeError: Cannot redefine property: x

// 第三步:即使 writable: false,value 还能被 configurable:false+writable:false 的属性修改吗?
// 不能,但这里有个微妙之处:
Object.defineProperty(obj, 'x', { value: 43 });
// TypeError:因为现在 writable 已经是 false,不能修改 value

规范解释:规范的设计理念是安全性单调递减(monotonically decreasing)。一旦你放弃了某个权限,就无法再拿回来。configurable: false 是放弃"重新配置"的权限,writable: true → false 是进一步放弃"写入"的权限,这符合最小权限原则。

陷阱3:访问器属性与数据属性不能共存

// 以下代码会抛出 TypeError
Object.defineProperty({}, 'x', {
  value: 1,
  get() { return 1; }
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute

// 同样不合法:
Object.defineProperty({}, 'x', {
  writable: true,
  set(v) {}
});
// TypeError

为什么规范要这样设计? 数据属性的 [[Value]] 和访问器属性的 [[Get]]/[[Set]] 在概念上是互斥的:数据属性直接存储值,访问器属性通过函数间接管理值。同时指定两者会产生歧义——赋值时应该调用 setter,还是直接修改 value?

// 正确方式:显式选择属性类型
// 数据属性:
Object.defineProperty(obj, 'x', { value: 1, writable: true });

// 访问器属性:
Object.defineProperty(obj, 'x', {
  get() { return this._x; },
  set(v) { this._x = v; }
});

// 可以从数据属性转换为访问器属性,但前提是 configurable: true
const obj = {};
Object.defineProperty(obj, 'x', { value: 1, writable: true, configurable: true });
Object.defineProperty(obj, 'x', { get() { return 42; } }); // 转换成功

陷阱4:原型上 writable:false 属性的赋值行为

这是最隐蔽的陷阱,很多有经验的开发者也会踩:

// 在原型上定义不可写属性
const proto = {};
Object.defineProperty(proto, 'x', {
  value: 1,
  writable: false,
  configurable: true,
  enumerable: true
});

const child = Object.create(proto);

// 非严格模式:
child.x = 99; // 静默失败!
console.log(child.x);         // 1 — 读的是原型上的值
console.log(child.hasOwnProperty('x')); // false — 没有创建自有属性

// 严格模式:
'use strict';
child.x = 99; // TypeError: Cannot assign to read only property 'x' of object '#<Object>'

完整的 [[Set]] 算法解读

当执行 child.x = 99 时:

  1. 检查 child 是否有自有属性 x → 没有
  2. child.[[Prototype]]proto,检查 proto 是否有 x → 有
  3. proto.x 是数据属性,且 writable: false
  4. 不论 receiver(child)是否可写,直接返回 false(不创建自有属性)
  5. 非严格模式:静默失败;严格模式:抛出 TypeError

关键点:这和 child 自身的 writable 无关——child 甚至没有这个属性。决定行为的是原型链上找到的那个属性writable 值。

// 对比:原型上 writable:true 时
const proto2 = {};
Object.defineProperty(proto2, 'y', { value: 1, writable: true, configurable: true });

const child2 = Object.create(proto2);
child2.y = 99; // 成功!在 child2 上创建自有属性,遮蔽原型
console.log(child2.y);                    // 99 — 自有属性
console.log(proto2.y);                    // 1 — 原型未变
console.log(child2.hasOwnProperty('y')); // true

陷阱5:Vue 2 为何无法检测数组下标赋值

这是 Vue 2 最著名的局限性,根本原因在于 Object.defineProperty 的工作方式:

// Vue 2 初始化数据时做了什么:
function observe(data) {
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key]);
  });
}

// 对于数组:
const vm = new Vue({
  data: { list: [1, 2, 3] }
});

// Vue 2 对 list 本身(作为属性)定义了 getter/setter
// 但 list[0]、list[1]、list[2] 没有被 defineProperty

问题的根源

// Vue 2 能检测的:
vm.list = [4, 5, 6]; // 触发 list 的 setter,Vue 重新 observe 新数组

// Vue 2 不能检测的:
vm.list[0] = 99;     // 直接修改数组索引,没有 setter 被触发
vm.list.length = 1;  // 同样无法检测

// 为什么不对每个索引 defineProperty?
// Vue 2 作者(尤雨溪)的解释:
// 1. 性能问题:数组可能有成千上万个元素
// 2. 无法检测未来添加的元素(push 之后的新索引)
// 3. 即使定义了,也只能检测"替换",无法检测"新增"

Vue 2 的解决方案:重写数组的 7 个变异方法:

const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayProto[method];
  Object.defineProperty(arrayMethods, method, {
    value(...args) {
      const result = original.apply(this, args);
      const ob = this.__ob__; // 观察者实例
      let inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args; break;
        case 'splice':
          inserted = args.slice(2); break;
      }
      if (inserted) ob.observeArray(inserted); // 对新元素建立响应式
      ob.dep.notify(); // 通知更新
      return result;
    },
    enumerable: false,
    writable: true,
    configurable: true
  });
});

Vue 3 的解决方案:用 Proxy 代替 Object.defineProperty(见 ch15),Proxy 的 set 陷阱可以拦截所有属性设置,包括任意索引和 length。

// Vue 3 的正确用法(不存在这个限制):
const state = reactive({ list: [1, 2, 3] });
state.list[0] = 99;    // 触发更新!
state.list.length = 1; // 触发更新!
state.list.push(4);    // 触发更新!

本章小结

  1. 每个属性有4个内部槽:数据属性的 value/writable/enumerable/configurable;访问器属性用 get/set 替换前两个。普通赋值创建的属性三个控制位全为 true,defineProperty 未指定的全为 false。

  2. configurable:false 是单向门:一旦设置就不能撤销,writable 只能从 true 改为 false,不能反向。enumerable 和类型(数据/访问器)都无法再改变。

  3. Object.freeze 是浅冻结:对嵌套对象无效,需要递归实现深度冻结。freeze 后数组 push 报 TypeError 是因为 length 属性也变为 writable:false。

  4. 原型链上的 writable:false 会阻止子对象创建自有属性:这是非严格模式下最隐蔽的"静默失败"来源。

  5. Vue 2 的数组限制来自 Object.defineProperty 本身:它无法检测数组下标赋值,Vue 2 通过重写数组变异方法绕过这个限制,Vue 3 用 Proxy 从根本上解决了这个问题。

本章评分
4.6  / 5  (28 评分)

💬 留言讨论