Object.freeze仅浅冻结,嵌套对象仍可修改;需递归冻结或改用扁平结构;ES模块const声明更可靠;冻结对象不适用于响应式数据。

Object.freeze 不能递归冻结嵌套对象
很多人以为 Object.freeze 能“彻底锁定”整个对象树,实际它只做浅冻结。如果常量里有嵌套对象或数组,内部属性依然可改:
const API_CONFIG = Object.freeze({
baseUrl: 'https://api.example.com',
headers: { 'Content-Type': 'application/json' }
});
API_CONFIG.headers['Content-Type'] = 'text/plain'; // ✅ 成功修改!
这是因为 headers 引用的对象本身没被冻结。真正防篡改必须手动递归处理,或改用更可控的结构。
- 优先用纯不可变数据结构:比如把
headers改成Object.freeze({ 'Content-Type': 'application/json' })再赋值 - 嵌套层级深时,写个简单递归 freeze 工具函数(注意避开循环引用)
- 更稳妥的做法是避免嵌套——把多层配置拆成扁平常量对象,每个都单独
Object.freeze
冻结后仍可删除/添加属性?不会,但 typeof 检查会失效
Object.freeze 确实禁止新增、删除、重配置属性,这点没问题。但容易被忽略的是:它不阻止你对已冻结对象调用 delete 或 Object.defineProperty —— 这些操作会静默失败(非严格模式下),或抛出 TypeError(严格模式)。问题在于,很多业务代码依赖 typeof obj.xxx !== 'undefined' 判断字段是否存在,而冻结不影响该判断逻辑,但一旦有人误操作试图修改深层值,结果难以追踪。
- 开发阶段开启严格模式(
'use strict'),让非法操作立刻报错,而不是悄悄忽略 - 不要依赖
delete是否成功来判断冻结状态;要用Object.isFrozen(obj)显式检查 - 冻结后若需“读取安全”,建议配合
Object.getOwnPropertyDescriptors验证关键字段的writable和configurable均为false
ES6+ 模块顶层常量比 Object.freeze 更可靠
真正需要“业务常量”的场景,比如 STATUS_CODE、ROLES、API_PATHS,直接用 const 声明 + 模块作用域,比手冻对象更自然也更安全:
export const STATUS_CODE = {
SUCCESS: 200,
NOT_FOUND: 404,
SERVER_ERROR: 500
};
Object.freeze(STATUS_CODE); // ✅ 可加,但非必需
因为 ES 模块绑定是只读的:即使没调用 Object.freeze,外部模块也无法重新赋值 STATUS_CODE,也不能给它加新属性(除非通过 Object.prototype 注入,那已是全局污染)。所以重点应放在“定义即冻结”的习惯上。
- 所有业务常量统一用
export const XXX = ...,不在运行时动态构造 - 避免从 API 或 localStorage 动态加载后再 freeze —— 那已失去“常量”语义
- 构建时可通过 TypeScript 的
as const或 Babel 插件进一步收窄类型,比运行时 freeze 更早暴露问题
Vue/React 中直接用冻结对象当响应式数据会出问题
框架内部依赖属性 setter 触发更新,而 Object.freeze 后对象所有属性 writable: false,导致响应式系统无法代理。常见现象:组件初次渲染正常,后续 props 或 store 中的冻结常量变化,视图不更新。
- 仅在非响应式上下文中使用冻结对象:如工具函数参数、配置校验、枚举比对等
- 如果必须作为响应式数据的一部分(比如 Vuex state 中的常量字段),要么不用 freeze,要么用
Object.assign({}, frozenObj)创建可代理副本 - 现代框架如 Vue 3 的
shallowRef或markRaw可显式跳过响应式转换,更适合包裹冻结常量
no-const-assign)协同解决。

















