先记住这个答案
Object.seal 让对象不能新增属性、不能删除属性,但已有属性的值还能改。Object.freeze 在它的基础上再进一步,把所有属性都变成只读,值也改不了。可以记成:seal 是把房间门锁上,屋里的东西还能挪;freeze 是连东西都焊死了。两者都只作用于对象第一层,而且在非严格模式下,违规操作不会报错,只是悄悄不生效。
seal:不能增删,能改值freeze:不能增删,也不能改值- 两者都是浅的,嵌套对象不受保护
- 严格模式下违规操作可能抛错
用一段代码看清两者的边界
同样一组操作,分别作用在 seal 和 freeze 过的对象上。
const sealed = Object.seal({ a: 1 })
sealed.a = 2 // 允许:改已有属性的值
sealed.b = 3 // 静默失败:不能新增
delete sealed.a // 静默失败:不能删除
console.log(JSON.stringify(sealed))
const frozen = Object.freeze({ a: 1, inner: { x: 1 } })
frozen.a = 2 // 静默失败:值也不能改
frozen.inner.x = 2 // 成功:freeze 只管第一层
console.log(frozen.a, frozen.inner.x)
console.log(Object.isSealed(frozen), Object.isFrozen(sealed))
;(function () {
'use strict'
try { frozen.a = 3 } catch (err) { console.log(err.name) }
})()查看输出与解释
{"a":2}
1 2
true false
TypeError前面几行在非严格模式下执行,失败的赋值和删除都没有报错,结果只能靠打印才看得出来。最后一段加了 'use strict',同样的赋值直接抛 TypeError。ES Module 和 class 内部默认就是严格模式,所以在现代项目里多数时候你会看到报错。
Object.isSealed(frozen) 是 true,说明冻结的对象一定也是密封的。反过来不成立,密封的对象属性还能写,所以不算冻结。
它们实际改了什么:属性描述符
每个属性背后都有一组描述符,其中 writable 控制值能不能改,configurable 控制属性能不能删、描述符能不能再改。对象本身还有一个「可扩展」标记,控制能不能加新属性。
三个方法就是在逐级关掉这些开关。Object.preventExtensions 只关掉可扩展。Object.seal 关掉可扩展,再把每个属性的 configurable 设成 false。Object.freeze 在 seal 的基础上,再把每个数据属性的 writable 设成 false。用 Object.getOwnPropertyDescriptor(sealed, 'a') 打印出来,能直接看到 writable: true, configurable: false。
还有一个细节:seal 之后,你仍然可以把某个属性从 writable: true 改成 false,但反过来改不回去。规范允许不可配置的属性往更严格的方向走一步。
- 数组也一样
Object.freeze([1, 2])之后,push、pop、splice这类会改数组的方法都会抛TypeError,而且不管是不是严格模式都会抛,因为这些方法内部用的是会报错的写入方式。密封的数组不能push,但arr[0] = 9可以。
容易答错的地方
- 以为
freeze之后对象完全不可变 freeze是浅的。frozen.inner.x = 2能成功,因为inner这个属性存的是一个引用,冻结的是引用不能换,引用指向的对象没被冻结。要深冻结,得递归地对每个对象属性调用Object.freeze。- 以为赋值失败一定会报错
- 非严格模式下只是静默失败。老项目里的普通脚本、控制台里直接敲的代码都是非严格模式,赋值「看起来成功了」,数据其实没变,这类问题很难排查。
- 分不清
seal和preventExtensions preventExtensions只禁止新增,属性还能删。seal连删除都禁止了。
面试官还会怎么问?
怎么实现一个深冻结?
先对当前对象调用 Object.freeze,再遍历 Reflect.ownKeys(obj),遇到值是对象且还没冻结的,就递归处理。用 Object.isFrozen 做判断,顺便能避免循环引用导致死循环。
冻结对象能被解冻吗?
不能,这三个操作都是单向的。想「修改」冻结对象,只能复制一份新的,比如 { ...frozen, a: 2 },得到的新对象是普通对象。
实际项目里什么时候用 freeze?
常量配置、枚举对象,以及强调不可变数据的状态管理。比如 Redux Toolkit 底层的 Immer 会自动冻结它生成的 state,谁直接改 state 就会立刻失败,问题能尽早暴露。Vue 里把大列表数据冻结后再交给响应式系统,Vue 会跳过它,不做深层代理,能省掉不少开销。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。