属性描述符、访问器,以及 freeze、seal、preventExtensions 有什么区别?

先理解原理和示例,再练习完整口述与追问。

数据属性有 value、writable、enumerable、configurable;访问器属性用 get 和 set 代替 value 和 writable。普通赋值创建的属性默认可写、可枚举、可配置。defineProperty 省略的标志默认为 false。preventExtensions 禁止加属性,seal 再禁止配置和删除,freeze 再禁止改值;三者都是浅层的。

每个自有属性都有描述符。数据属性能不能改值看 writable,for...in 和 Object.keys 看 enumerable,能不能删除或改描述符看 configurable。访问器属性在读取时调用 get、赋值时调用 set,this 是访问时所在的对象,所以放在原型上时,this 仍是实例。Object.assign 和展开运算符读的是 get 的返回值,复制出来的是数据属性,不会把 getter 原样搬过去。

Object.defineProperty 与字面量、赋值的默认值不同。给已有对象补属性时,没写的 enumerable、writable、configurable 都是 false,所以它经常“定义了但 Object.keys 看不到”。configurable 为 false 之后,除了 writable 从 true 改成 false,其他标志都不能再改。

Object.preventExtensions 只禁止添加新属性,已有属性仍可改可删。Object.seal 在此基础上把全部自有属性的 configurable 设为 false,不能删除,但仍可改可写属性的值。Object.freeze 再把数据属性的 writable 设为 false。三者都不递归,嵌套对象仍可修改;严格模式下违反限制会抛 TypeError,非严格模式可能静默失败。判断用 isExtensible、isSealed、isFrozen。

js
const user = { profile: { city: 'HZ' } }
Object.freeze(user)
user.profile.city = 'SH'
console.log(user.profile.city) // 'SH',freeze 没有冻住内层
console.log(Object.isFrozen(user), Object.isFrozen(user.profile)) // true false

const counter = {
  n: 0,
  get doubled() { return this.n * 2 }
}
const copy = { ...counter }
console.log(Object.getOwnPropertyDescriptor(copy, 'doubled')) // value: 0,不再是 getter

const 只禁止重新绑定变量,不冻结对象。freeze 之后属性仍可枚举。Vue2 无法把 freeze 过的对象变成响应式,Vue3 的 markRaw / shallowRef 是另一套跳过代理的机制。

数据属性看 value、writable、enumerable、configurable,访问器属性用 get 和 set。普通赋值默认三项都是 true,defineProperty 省略时默认是 false。preventExtensions 不能加新属性,seal 还不能删除和改描述符,freeze 再把值也锁住。它们都只作用于这一层,嵌套对象还要自己处理。展开和 Object.assign 会把 getter 读成普通值,不会复制访问器本身。