你正在给一家 SaaS 客户做「企业级仪表盘」项目。
需求很简单:把 20 多个子系统的实时指标汇总到一张大屏。
为了"图省事",你在入口文件里顺手写了这么一段:
js
// main.js
window.dashboard = {}; // 🔍 全局缓存
window.dashboard.cache = new Map(); // 🔍 存接口数据
window.dashboard.refresh = () => { // 🔍 供子系统调用
fetch('/api/metrics').then(r => r.json())
.then(data => window.dashboard.cache = new Map(data));
};
上线两周后,客户反馈:
"切到别的标签页再回来,大屏偶尔会白屏,刷新又好了。"
排查发现,Chrome 在内存紧张时会 回收后台标签页的 JS 堆 ,但 window
对象属于宿主环境,不会被回收 。
结果:
- 旧的
dashboard
引用还在,但闭包里的Map
被 GC 清掉,变成空壳; - 子系统继续调用
window.dashboard.refresh
,拿到的永远是空数据; - 页面逻辑崩溃,白屏。
解决方案:三层防护,把风险降到 0
1. 表面用法:用 WeakMap
做"软引用"
js
// 把全局缓存改成弱引用
const cache = new WeakMap(); // 🔍 不阻止 GC
window.dashboard = {
get data() { return cache.get(document); }, // 🔍 与当前文档生命周期绑定
set data(v) { cache.set(document, v); }
};
这样当用户切走标签页、文档被卸载时,cache
自动释放,切回来重新初始化,白屏消失。
2. 底层机制:为什么 window
不会被 GC?
层级 | 角色 | 生命周期 | 是否可被 GC |
---|---|---|---|
JS 堆 | 闭包、普通对象 | 无引用即回收 | ✅ |
Host 环境 | window |
与标签页同生同灭 | ❌ |
渲染进程 | GPU 纹理、DOM | 标签页关闭后统一清理 | ✅ |
结论:window
是宿主对象,生命周期长于 JS 堆,挂上去的东西如果强引用 JS 对象,会导致"僵尸引用"。
3. 设计哲学:把"全局"变成"注入"
与其在 window
上挂变量,不如用 依赖注入 把作用域限制在模块内部:
js
// dashboard.js
export const createDashboard = () => {
const cache = new Map(); // 🔍 私有状态
return {
refresh: () => fetch('/api/metrics')
.then(r => r.json())
.then(data => cache.clear() && data.forEach(d => cache.set(d.id, d)))
};
};
js
// main.js
import { createDashboard } from './dashboard.js';
const dashboard = createDashboard(); // 🔍 生命周期由模块控制
应用扩展:可复用的配置片段
如果你的项目必须暴露全局 API(比如给第三方脚本调用),可以用 命名空间 + Symbol 双保险:
js
// global-api.js
const NAMESPACE = Symbol.for('__corp_dashboard__');
window[NAMESPACE] = { // 🔍 避免命名冲突
version: '2.1.0',
createDashboard
};
环境适配说明:
- 浏览器:Symbol 在 IE11 需 polyfill;
- Node:同构渲染时
window
不存在,用globalThis[NAMESPACE]
兜底。
举一反三:3 个变体场景
- 微前端基座
用window.__MICRO_APP_STORE__
传递状态,但内部用Proxy
做读写拦截,防止子应用直接篡改。 - Chrome 插件 content-script
通过window.postMessage
与页面通信,避免污染宿主全局。 - Electron 主进程/渲染进程
用contextBridge.exposeInMainWorld('api', safeApi)
把 API 注入到隔离上下文,防止 Node 权限泄露。
小结:
把 window
当"公告栏"可以,但别把"保险箱"也挂上去。
用弱引用、模块化和依赖注入,才能把全局变量的风险真正关进笼子里。