在现代 Web 开发中,我们经常会引入第三方脚本——广告、统计、SDK、插件等。这些脚本为了“方便”,常常直接向全局作用域(window 对象)注入变量、函数甚至修改原生对象。这种行为不仅会污染全局命名空间,还可能导致变量冲突、性能下降、安全风险,甚至悄无声息地篡改关键 API(比如重写 console.log 或 XMLHttpRequest)。
那么,作为开发者,我们如何准确识别一个第三方脚本到底往 window 上加了什么、改了什么?本文将带你手把手实现一个轻量级但可靠的监控方案。
一、问题场景
假设我们要加载一个广告脚本:
<script src="https://cdn.wwads.cn/js/makemoney.js"></script>
我们想知道:
- 它新增了哪些全局变量?
- 它是否修改了已有的全局属性(比如
Array、Promise或自定义变量)?
仅靠 Object.keys(window).length 只能知道数量变化,无法定位具体内容。我们需要更精细的对比。
二、核心思路:快照 + 差异对比
要检测“变化”,最直接的方法是:
- 加载脚本前,对
window做一次完整快照(属性名 + 值)。 - 加载脚本后,再做一次快照。
- 对比两次快照,找出新增项和值发生变化的项。
关键点:
- 使用
Object.getOwnPropertyNames(window)获取所有属性(包括不可枚举的)。 - 使用
Map存储属性名与值的映射,便于高效查找。 - 使用
Object.is()进行值比较,避免NaN、-0/+0等边界问题。 - 包裹
try...catch,防止访问某些特殊属性时抛出异常。
三、完整实现代码
// 1. 创建 window 快照:记录所有属性及其当前值
function snapshotWindow() {
const props = Object.getOwnPropertyNames(window);
const snap = new Map();
for (const key of props) {
try {
snap.set(key, window[key]);
} catch (e) {
// 某些属性(如跨域 iframe 中的 opener)访问会报错
snap.set(key, '<ACCESS_ERROR>');
}
}
return snap;
}
// 2. 记录加载前的状态
const beforeSnapshot = snapshotWindow();
// 3. 动态加载第三方脚本
const script = document.createElement('script');
script.src = 'https://cdn.wwads.cn/js/makemoney.js';
// 4. 脚本加载完成后进行对比
script.onload = () => {
// 稍作延迟,确保脚本完全执行(含微任务)
setTimeout(() => {
const afterSnapshot = snapshotWindow();
const added = [];
const modified = [];
// 遍历加载后的所有属性
for (const [key, newValue] of afterSnapshot) {
if (!beforeSnapshot.has(key)) {
added.push(key); // 新增
} else {
const oldValue = beforeSnapshot.get(key);
if (!Object.is(oldValue, newValue)) {
modified.push({ key, oldValue, newValue }); // 被修改
}
}
}
console.log('🟢 新增的全局变量:', added);
console.log('🟡 被修改的全局变量:', modified);
}, 100);
};
script.onerror = () => console.error('第三方脚本加载失败');
document.head.appendChild(script);
四、输出示例与解读
运行上述代码后,控制台可能输出:
🟢 新增的全局变量: ['_AdBlockInit', '_IsTrustedClick']
🟡 被修改的全局变量: [
{ key: "isMobile", oldValue: [Function], newValue: [Object] }
]
这意味着:
- 脚本注入了
_AdBlockInit等广告相关变量; - 它可能重写了
isMobile对象。
这类行为在广告或数据采集脚本中并不罕见,但对应用稳定性和安全性构成潜在威胁。
五、注意事项与进阶建议
1. 动态属性误报
某些 window 属性是 getter(如 window.devicePixelRatio),每次读取值可能不同。这类属性可能被误判为“修改”。可建立白名单过滤:
const DYNAMIC_PROPS = new Set(['devicePixelRatio', 'innerWidth', 'innerHeight', 'scrollY']);
if (DYNAMIC_PROPS.has(key)) return; // 跳过对比
2. 深度修改无法检测
如果脚本只修改了某对象的内部属性(如 window.myLib.config = {...}),而 myLib 引用未变,则不会被识别。如需深度监控,可结合 Proxy 或定期遍历,但代价较高。
3. 生产环境慎用
此方案主要用于开发调试或安全审计,不建议在生产环境长期运行,因其会遍历整个 window,有一定性能开销。
4. 结合 CSP 与沙箱
对于高安全要求的场景,建议:
- 使用 Content Security Policy (CSP) 限制脚本来源;
- 将第三方脚本运行在 iframe 沙箱 中,彻底隔离全局作用域。
5. 隔离方案:运行后自动恢复关键全局变量
如果你已知某些第三方脚本会覆盖特定全局变量(如 isMobile 或 config),但又无法避免加载该脚本,可以采用“备份-恢复”策略,在脚本执行后自动还原关键变量,避免副作用扩散:
function loadThirdPartyScript(src, backupGlobals = []) {
const backup = {};
backupGlobals.forEach(key => {
backup[key] = window[key];
});
const script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = script.onerror = () => {
backupGlobals.forEach(key => {
window[key] = backup[key];
});
};
document.head.appendChild(script);
}
// 使用示例
loadThirdPartyScript('https://cdn.wwads.cn/js/makemoney.js ', ['isMobile', 'config']);
⚠️ 注意:此方法仅适用于脚本同步执行完毕后立即产生副作用的场景。若脚本通过
setTimeout、事件监听器或异步回调持续修改全局变量,则恢复操作可能失效。
六、总结
第三方脚本如同“黑盒”,我们无法控制其实现,但可以主动监控其副作用。通过简单的快照对比,就能清晰看到它对全局环境的“入侵”行为,为性能优化、安全审计和故障排查提供有力依据。
下次当你引入一个未知脚本时,不妨先问一句:“你到底往 window 上写了什么?”
💡 小技巧:将上述逻辑封装为一个函数
detectGlobalPollution(scriptUrl),可复用于任何第三方脚本的检测。
本文链接:https://toolshu.com/article/f5gr8qsc
本作品采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。


加载中...