模块默认只读但属性可变,需用 deepFreeze 递归冻结嵌套对象;Object.freeze 仅冻结顶层,无法防护原型污染或 Proxy 绕过,不能替代服务端校验。

在 type="module" 环境中,模块默认是只读的——导出对象本身不能被重新赋值(比如 exportedObj = {} 会报错),但其内部属性仍可被修改。要真正防止恶意篡改(如改写方法、添加/删除属性),必须对导出对象做深度冻结。
用 Object.freeze 冻结顶层导出对象
仅冻结顶层对象只能阻止属性增删和配置变更,无法递归冻结嵌套对象或函数。适合简单扁平结构:
// utils.js
const utils = {
version: '1.0',
log: (msg) => console.log(msg),
config: { timeout: 5000 }
};
// 冻结顶层,但 config 仍是可变的
export default Object.freeze(utils);
这样 utils.version = '2.0' 或 delete utils.log 会失败,但 utils.config.timeout = 1000 依然生效。
实现深度冻结(Deep Freeze)
需要递归冻结所有嵌套对象和数组。可用轻量级工具函数:
- 遍历对象所有自有属性(包括 symbol)
- 对每个属性值:若为对象且非 null、非原始类型、未冻结,则递归 freeze
- 最后调用
Object.freeze()锁定当前对象
// deepFreeze.js
export function deepFreeze(obj) {
if (obj && typeof obj === 'object') {
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null && typeof obj[prop] === 'object') {
deepFreeze(obj[prop]);
}
});
}
return Object.freeze(obj);
}
然后在导出前使用:
// api.js
import { deepFreeze } from './deepFreeze.js';
export const API = deepFreeze({
baseUrl: 'https://api.example.com',
endpoints: {
users: '/users',
posts: '/posts'
},
fetch: async (url) => fetch(url)
});
配合 export default + 命名导出双重保护
模块内避免直接暴露可变引用。推荐统一通过冻结后的对象导出:
- 不用
export let或export var—— 它们允许外部重赋值(即使加const修饰,对象内容仍可变) - 优先用
export default Object.freeze({...})或export const X = deepFreeze(...) - 若需命名导出多个项,可统一收口到一个冻结对象中,再解构导出
// constants.js
const CONSTS = Object.freeze({
MAX_RETRY: 3,
TIMEOUT: 10000,
ERRORS: Object.freeze({
NETWORK: 'NETWORK_ERROR',
AUTH: 'AUTH_FAILED'
})
});
export const { MAX_RETRY, TIMEOUT, ERRORS } = CONSTS;
// 或直接 export default CONSTS;
注意:冻结不能阻止原型污染和 Proxy 绕过
Object.freeze 是运行时防护,不是安全沙箱:
- 无法防御通过
Object.prototype注入属性(需配合Object.setPrototypeOf(obj, null)或严格模式下禁用原型访问) - 无法拦截
Proxy对象的劫持行为——如果模块返回的是 Proxy,冻结无效 - 不能替代服务端校验或权限控制,仅用于提升客户端代码健壮性
真正关键的逻辑和敏感数据,仍应放在服务端处理。


















