先记住这个答案
Object.preventExtensions(obj) 把对象标记为不可扩展,之后不能再新增属性。非严格模式下写 obj.newKey = 1 什么也不会发生,严格模式下会抛 TypeError。已有的属性不受影响,可以改值,也可以 delete。这个操作是单向的,没有办法恢复成可扩展。判断用 Object.isExtensible(obj),普通对象默认返回 true,处理过之后返回 false。
- 只禁止新增属性,修改和删除都不受影响
- 严格模式下新增抛
TypeError,非严格静默失败 - 操作不可逆,也不能再修改对象的原型
- 禁止新增属性,但还能改现有属性
看代码:能改能删,就是不能加
下面对一个普通对象调用 preventExtensions,分别试试新增、修改、删除。
const user = { name: 'Tom', age: 18 }
Object.preventExtensions(user)
user.email = 'a@b.com' // 非严格模式下静默失败,不报错
user.age = 20 // 修改已有属性,没问题
delete user.name // 删除也没问题
console.log(JSON.stringify(user), Object.isExtensible(user))
try {
Object.defineProperty(user, 'email', { value: 'x' }) // 换个方式加也不行
} catch (e) {
console.log(e.name)
}
console.log(Object.isSealed(user), Object.isFrozen(user))查看输出与解释
{"age":20} false
TypeError
false false第一行能看出 email 没加上,age 改了,name 删了。第二行说明不可扩展是对象本身的状态,不管是赋值还是 defineProperty,只要是新增属性都会被拦下,而且 defineProperty 不管严格模式与否都会抛错。第三行说明它只是最松的一级,还算不上密封或冻结,因为属性依然可删、可改。
它和 seal、freeze 是什么关系
可以把这三个方法看成逐级加码。preventExtensions 只关上「新增」这扇门;seal 在此基础上把所有属性的 configurable 设成 false,于是也不能删了;freeze 再把所有数据属性的 writable 设成 false,连值都不能改。
所以被 seal 或 freeze 的对象,Object.isExtensible 也一定返回 false。反过来不成立,不可扩展的对象不一定是密封的。有个特例很适合在面试里提:空对象被 preventExtensions 之后,Object.isSealed 和 Object.isFrozen 都返回 true,因为它没有任何属性可以删或改,条件自然都满足。
原型也被锁住了
不可扩展除了不能加属性,还有一条限制:不能再修改对象的原型。调用 Object.setPrototypeOf(obj, other) 会抛 TypeError,除非传的就是它当前的原型。
但这把锁只锁对象自己。它的原型对象还是可扩展的,往 Object.prototype 或类的原型上加方法,这个对象通过原型链照样能调用到。所以 preventExtensions 不能当作「对象的能力从此固定」来用。
容易答错的地方
- 以为
preventExtensions会让属性只读 - 不会。它只管新增,已有属性的
writable和configurable一点没变。要只读得用freeze,或者用defineProperty把单个属性设成writable: false。 - 以为可以再调一个方法恢复
- 没有
Object.allowExtensions这种方法。不可扩展是单向的,需要可扩展的版本只能复制一份,比如{ ...obj },新对象是可扩展的。 - 忽略严格模式的差别
- 同一句
obj.x = 1,在模块或class里是严格模式,会直接抛错;放在普通脚本里就静默失败。线上出现「属性怎么没加上也不报错」,多半是这个原因。
面试官还会怎么问?
对原始值调用 Object.isExtensible 会怎样?
ES2015 以后不会报错,原始值被当作不可扩展的对象,返回 false。Object.preventExtensions(1) 也不报错,原样返回 1。但 Reflect.preventExtensions(1) 会抛 TypeError,Reflect 的方法要求传对象。
数组被 preventExtensions 后能 push 吗?
不能。push 本质是在新的索引上新增属性,会抛 TypeError,规范规定 push 内部写入失败时一律抛错,和代码是否严格模式无关。但 pop 可以,因为删除元素是允许的。
实际项目里什么时候用它?
用得不多。常见场景是给配置对象、插件上下文加一层保护,防止使用方拼错属性名却不自知。更常见的做法是直接用 Object.freeze,或者在开发环境用 Proxy 拦截未知属性并报警。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。