前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库Object.freeze 浅冻结与深度冻结实现
JavaScript对象与原型

Object.freeze 冻住的对象,为什么里面的属性还能改?深度冻结怎么写?

Object.freeze 是浅冻结,只锁住对象自己这一层的属性,属性值如果是另一个对象,那个对象照样能改。深度冻结要递归地把每一层都 freeze 一遍,并用 WeakSet 记录处理过的对象,防止循环引用导致死循环。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#对象与原型
先看核心答案读代码示例
理解线索

冻结到底锁住了什么

  1. 属性本身所有自有属性变成不可写、不可配置,不能再增删
  2. 属性值指向的对象只是一个引用,引用不能换,但对象内部没被冻结
  3. Object.isFrozen检查某一层是否被冻结,要逐层看

冻结是不可逆的,一个对象被 freeze 之后没有办法解冻,只能复制一份新的出来改。

核心回答

先记住这个答案

Object.freeze(obj) 会让 obj 自己的属性不能新增、删除和修改,但它只处理这一层。如果某个属性值是对象,冻结的只是「这个属性指向哪个对象」,被指向的对象内部仍然可以改。要做到整棵对象树都不可变,就要自己写深度冻结:遍历每个属性,是对象就递归冻结,同时记录已经处理过的对象,避免遇到循环引用时无限递归。

  • freeze 只冻第一层,嵌套对象不受影响
  • 冻结后不能增、删、改属性,也不能换原型
  • 非严格模式静默失败,严格模式抛 TypeError
  • 深度冻结要递归,并用 WeakSet 防循环引用

冻住了外层,里层还能改

最常见的场景是把配置对象冻结起来,防止被人误改。看看实际效果。

浅冻结的效果JavaScript
const config = { api: '/v1', retry: { times: 3 } }
Object.freeze(config)

config.api = '/v2' // 非严格模式下静默失败
config.retry.times = 5 // 能改成功,retry 没被冻结

console.log(config.api, config.retry.times)
console.log(Object.isFrozen(config), Object.isFrozen(config.retry))
查看输出与解释
/v1 5
true false

config.api 没改掉,而且没有任何报错。这是非严格模式下的行为,写入失败时静默忽略。在严格模式(ES Module 和 class 内部默认就是严格模式)下,同样的赋值会抛 TypeError,更容易发现问题。 config.retry.times 改成功了,因为 retry 属性里存的只是一个引用。freeze 让这个引用不能换成别的对象,但引用指向的对象本身没被冻结,Object.isFrozen(config.retry) 返回 false 就说明了这点。 freeze 具体做了三件事:把对象标记为不可扩展(不能加新属性),把所有自有属性设为不可配置(不能删、不能改描述符),把数据属性设为不可写。另外冻结后原型也不能再换,Object.setPrototypeOf 会失败。

手写深度冻结

思路很直接:先冻结当前对象,再对每个属性值递归调用。难点在细节。

带循环引用保护的实现JavaScript
function deepFreeze(obj, seen = new WeakSet()) {
  // 原始值不用冻结;处理过的对象直接返回,防止循环引用死循环
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) return obj
  if (seen.has(obj)) return obj
  seen.add(obj)

  // Reflect.ownKeys 能拿到 Symbol 键和不可枚举的键
  for (const key of Reflect.ownKeys(obj)) {
    const desc = Object.getOwnPropertyDescriptor(obj, key)
    if ('value' in desc) deepFreeze(desc.value, seen) // 跳过 getter,不去触发它
  }
  return Object.freeze(obj)
}

这里有三个容易漏的地方。第一,用 Reflect.ownKeys 而不是 Object.keys,后者会漏掉 Symbol 键和不可枚举的键。第二,用描述符取值而不是直接 obj[key],因为属性可能是 getter,直接读会执行 getter 里的代码,甚至产生副作用。第三,用 WeakSet 记录访问过的对象,a.self = a 这种循环引用不加判断就会栈溢出。

不要用 Object.isFrozen 代替 seen
有人会写成「已经冻结的就跳过」。但对象可能在别处被浅冻结过,外层是冻结状态,里层还没冻,这样一跳过里层就漏了。用 WeakSet 记录「自己处理过」才可靠。

什么时候真的需要冻结

冻结是运行时的保护,每一层都要遍历一次,大对象深度冻结有成本。常见用法是冻结全局常量、枚举对象、共享的默认配置,防止某处代码不小心改了影响全局。

状态管理里的不可变数据是另一种思路。Redux Toolkit 用的 Immer 默认会冻结产出的 state,有人绕过它直接修改时,修改不会生效(严格模式下直接报错),问题能尽早暴露。而 TypeScript 的 readonly 和 as const 只在编译期检查,运行时对象还是能改,和 freeze 是互补的关系。

回答前,多想一步

容易答错的地方

以为 const 加 freeze 就完全不可变
const 只保证变量不能重新赋值,freeze 只保证第一层属性不能改。两者叠加,嵌套对象还是能改。要完全不可变只能深度冻结。
以为冻结失败会报错
非严格模式下给冻结对象赋值、删属性都是静默失败,代码继续往下跑。排查「为什么赋值没生效」时,冻结对象是一个常被忽略的原因。
以为 freeze 能冻住 Map 和 Set 的内容
Map 和 Set 的数据存在内部结构里,不是对象属性。冻结一个 Map 之后,map.set() 照样能加数据。
试着用自己的话回答

面试官还会怎么问?

freeze、seal、preventExtensions 有什么区别?

preventExtensions 只禁止新增属性。seal 在此基础上再禁止删除和重新配置,但已有属性的值还能改。freeze 最严格,连值也不能改。三者都是浅的。

冻结的数组能 push 吗?

不能。push 要新增索引并修改 length,都会失败,而且 push 内部按严格规则执行,即使在非严格模式下也会抛 TypeError。

冻结了的对象还想修改,怎么办?

冻结不可逆,只能复制一份再改,比如 { ...config, api: '/v2' }。展开得到的新对象没有被冻结,嵌套对象还是和原对象共用的同一个,如果原来深度冻结过,改嵌套属性前也要一起复制。

从一道题,走向一组知识

把知识连起来

对象与原型

Reflect.ownKeys 返回的键是什么顺序?和 Object.keys 有什么区别?

深度冻结遍历属性时要用 Reflect.ownKeys,否则会漏掉 Symbol 键和不可枚举的键。

对象与原型

Reflect.set 和直接用等号赋值,有什么不一样?

给冻结对象赋值会静默失败,换成 Reflect.set 能拿到 false,知道写入没成功。

参考资料

  • Object.freeze()

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 冻住了外层,里层还能改
  3. 手写深度冻结
  4. 什么时候真的需要冻结
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑