Object 静态方法与 Hook 使用场景
Object 的静态方法是 JS 逆向中 Hook 技术的底层基础。理解每个方法的能力边界,直接决定你能不能绕过检测、能不能稳定注入、能不能还原被篡改的环境。 descriptor 两种形式(不可混用): 部分网站检测 fn.toString() 是否包含 native code: 一次性定义多个属性,等同于多次调用 defineProperty。 获取对象自身某个属性的完整描述符对象,是 Hook 前的必要准备动作。 见上方 defineProperty 场景 2,核心就是先用这个方法取到原生 getter/setter,再包一层。 返回对象所有自身属
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
适用版本:ES5+(2026-05-08 核实)
Object 的静态方法是 JS 逆向中 Hook 技术的底层基础。理解每个方法的能力边界,直接决定你能不能绕过检测、能不能稳定注入、能不能还原被篡改的环境。
目录
- Object.defineProperty
- Object.defineProperties
- Object.getOwnPropertyDescriptor
- Object.getOwnPropertyDescriptors
- Object.getPrototypeOf / Object.setPrototypeOf
- Object.create
- Object.freeze / Object.isFrozen
- Object.seal / Object.isSealed
- Object.preventExtensions / Object.isExtensible
- Object.getOwnPropertyNames
- Object.getOwnPropertySymbols
- Object.assign
- Object.keys / Object.values / Object.entries
- Object.hasOwn
- Object.fromEntries
- 综合场景:检测与反检测对抗
Object.defineProperty
基础
Object.defineProperty(obj, prop, descriptor)
descriptor 两种形式(不可混用):
| 字段 | 数据描述符 | 访问器描述符 |
|---|---|---|
value |
属性值 | — |
writable |
是否可写 | — |
get |
— | getter 函数 |
set |
— | setter 函数 |
enumerable |
是否可枚举(for...in / Object.keys) | 同左 |
configurable |
是否可再次 defineProperty / delete | 同左 |
Hook 场景 1:拦截属性写入(定位赋值来源)
// 监控 window.sign 被谁写入
(function () {
let _val;
Object.defineProperty(window, 'sign', {
configurable: true,
enumerable: true,
get() { return _val; },
set(v) {
console.log('[sign SET]', v, new Error().stack);
_val = v;
}
});
})();
Hook 场景 2:Cookie 写入监控(带条件断点)
(function () {
// 先从 Document.prototype 获取原生访问器,避免循环调用
const desc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
const nativeGet = desc.get;
const nativeSet = desc.set;
Object.defineProperty(document, 'cookie', {
configurable: true,
get() { return nativeGet.call(document); },
set(v) {
if (v.includes('_token')) debugger; // 条件断点
console.log('[cookie SET]', v);
nativeSet.call(document, v);
}
});
})();
直接用
document.cookie取原生访问器会触发自己的 getter,必须从Document.prototype上取。
Hook 场景 3:将 configurable 设为 false 来阻止被二次 Hook
// 锁死某个属性,让别人无法再次 defineProperty
Object.defineProperty(window, 'navigator', {
configurable: false, // 锁死
get() { return myFakeNavigator; }
});
// 此后再 defineProperty 会抛 TypeError: Cannot redefine property: navigator
Hook 场景 4:伪造 toString 防检测
部分网站检测 fn.toString() 是否包含 [native code]:
const _fetch = window.fetch;
window.fetch = function (...args) {
console.log('[fetch]', args[0]);
return _fetch.apply(this, args);
};
// 让 toString 返回原生字符串
Object.defineProperty(window.fetch, 'toString', {
value: () => 'function fetch() { [native code] }',
configurable: false
});
// 或者更简单:
window.fetch.toString = _fetch.toString.bind(_fetch);
踩坑
configurable: false之后无法再修改描述符,也无法delete,Hook 注入必须在此之前- 数据描述符和访问器描述符不能混合,已经是数据描述符的属性若要改成访问器,必须先
configurable: true writable: false的属性严格模式下赋值抛错,非严格模式下静默失败
Object.defineProperties
一次性定义多个属性,等同于多次调用 defineProperty。
Object.defineProperties(obj, {
prop1: { value: 1, writable: true, enumerable: true, configurable: true },
prop2: { get() { return this._p2; }, set(v) { this._p2 = v; } }
})
Hook 场景:批量伪造 navigator 属性
// 补环境时批量设置 navigator 的各个字段
Object.defineProperties(navigator, {
webdriver: {
configurable: true,
get: () => false // 关键:隐藏自动化标志
},
platform: {
configurable: true,
get: () => 'Win32'
},
languages: {
configurable: true,
get: () => ['zh-CN', 'zh', 'en']
},
plugins: {
configurable: true,
get: () => fakePluginArray // 伪造插件列表
}
});
Object.getOwnPropertyDescriptor
获取对象自身某个属性的完整描述符对象,是 Hook 前的必要准备动作。
const desc = Object.getOwnPropertyDescriptor(obj, prop);
// 返回: { value, writable, enumerable, configurable }
// 或: { get, set, enumerable, configurable }
// 属性不存在时返回 undefined
Hook 场景 1:安全 Hook cookie(取原生访问器)
见上方 defineProperty 场景 2,核心就是先用这个方法取到原生 getter/setter,再包一层。
const cookieDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
// cookieDesc.get → 原生 cookie getter
// cookieDesc.set → 原生 cookie setter
Hook 场景 2:检测属性是否可被 Hook
function canHook(obj, prop) {
const desc = Object.getOwnPropertyDescriptor(obj, prop);
if (!desc) return true; // 不存在,可定义
return desc.configurable === true; // configurable 为 true 才能重定义
}
if (canHook(window, 'XMLHttpRequest')) {
// 安全 Hook
} else {
console.warn('XMLHttpRequest 已被锁死,无法 Hook');
}
Hook 场景 3:完整保存原始描述符用于恢复
// 保存原始描述符
const originalDesc = Object.getOwnPropertyDescriptor(window, 'localStorage');
// 执行 Hook
Object.defineProperty(window, 'localStorage', { /* ... */ });
// 需要恢复时
Object.defineProperty(window, 'localStorage', originalDesc);
Object.getOwnPropertyDescriptors
返回对象所有自身属性的描述符集合(以对象形式)。
const descs = Object.getOwnPropertyDescriptors(obj);
// { prop1: descriptor1, prop2: descriptor2, ... }
Hook 场景 1:深拷贝原型(保留 getter/setter)
普通的 Object.assign 会丢失 getter/setter,只拷贝值:
// 错误做法:getter/setter 会被求值后复制为普通值
const bad = Object.assign({}, sourceProto);
// 正确做法:完整保留描述符
const copy = Object.create(
Object.getPrototypeOf(sourceProto),
Object.getOwnPropertyDescriptors(sourceProto)
);
Hook 场景 2:扫描对象,找出所有访问器属性(可能是 Hook 埋点)
function findAccessors(obj) {
const descs = Object.getOwnPropertyDescriptors(obj);
return Object.entries(descs)
.filter(([, d]) => typeof d.get === 'function' || typeof d.set === 'function')
.map(([key, d]) => ({ key, get: !!d.get, set: !!d.set }));
}
// 检测 window 上有哪些属性被定义了 getter/setter(可能是反调试注入)
console.table(findAccessors(window));
Hook 场景 3:一次性复制整个对象的 Hook 状态到另一个对象
// 将 document 的所有属性描述符整体复制到 fakeDoc(补环境常用)
const fakeDoc = Object.create(null);
Object.defineProperties(fakeDoc, Object.getOwnPropertyDescriptors(document));
Object.getPrototypeOf / Object.setPrototypeOf
Object.getPrototypeOf(obj) // 等同于 obj.__proto__,但更标准
Object.setPrototypeOf(obj, proto) // 修改原型链
Hook 场景 1:检测对象是否是某个类的实例(绕过 instanceof 检测)
某些网站用 obj instanceof XMLHttpRequest 检测请求是否被代理:
const fakeXHR = {};
Object.setPrototypeOf(fakeXHR, XMLHttpRequest.prototype);
// 现在 fakeXHR instanceof XMLHttpRequest === true
Hook 场景 2:通过原型链 Hook 批量拦截所有实例
Hook 原型上的方法,比 Hook 每个实例更高效:
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
console.log('[XHR open]', method, url);
return _open.apply(this, arguments);
};
// 所有已存在的和未来创建的 XHR 实例都会被拦截
Hook 场景 3:检测 Proxy 对象(Proxy 无法伪装 getPrototypeOf 陷阱之外的行为)
// 某些反爬代码通过原型链检测 Proxy
function isProxy(obj) {
try {
// Proxy 在 getPrototypeOf 上可以返回任意值,但 Object.getPrototypeOf 绕过陷阱
return Object.getPrototypeOf(obj) !== Object.getPrototypeOf(Object.getPrototypeOf(obj));
} catch {
return true;
}
}
Object.create
Object.create(proto, propertiesObject?)
// 创建一个以 proto 为原型的新对象,可选第二参数同 defineProperties 的格式
Hook 场景 1:创建干净的无原型对象作为 Map 使用
// 避免原型上的 hasOwnProperty/toString 等方法干扰 key 查找
const store = Object.create(null);
store['constructor'] = 'someValue'; // 不会有冲突
Hook 场景 2:补环境——创建带正确原型的假对象
// 创建一个假的 navigator,原型链指向真实 Navigator.prototype
const fakeNavigator = Object.create(Navigator.prototype, {
userAgent: { get: () => 'Mozilla/5.0 ...', configurable: true },
webdriver: { get: () => false, configurable: true },
platform: { get: () => 'Win32', configurable: true },
});
// navigator instanceof Navigator === true,通过原型检测
Object.defineProperty(window, 'navigator', {
get: () => fakeNavigator,
configurable: true
});
Hook 场景 3:创建继承了原始对象所有方法的"代理壳"
// 创建一个和 localStorage 同原型的假对象,但可以拦截所有方法
const fakeStorage = Object.create(Storage.prototype);
['getItem', 'setItem', 'removeItem', 'clear'].forEach(method => {
fakeStorage[method] = function (...args) {
console.log(`[localStorage.${method}]`, ...args);
return localStorage[method].apply(localStorage, args);
};
});
Object.freeze / Object.isFrozen
freeze 让对象同时满足:configurable: false、writable: false、不可新增属性。冻结是浅冻结,嵌套对象不受影响。
Object.freeze(obj)
Object.isFrozen(obj) // 返回 boolean
Hook 场景 1:保护 console 不被网站覆盖
// 在页面脚本执行前冻结 console,防止网站用 console.log = ()=>{} 屏蔽调试输出
Object.freeze(console);
// 此后 console.log = xxx 静默失败(严格模式抛错)
Hook 场景 2:检测对象是否被冻结(判断是否可以 Hook)
if (Object.isFrozen(window.crypto)) {
console.warn('crypto 已被冻结,无法直接 Hook,需要用 Proxy 替换整个对象');
} else {
// 可以直接 defineProperty
}
Hook 场景 3:冻结后的对象只能用 Proxy 整体替换
// 假设 window.performance 被冻结了
const _performance = window.performance;
const proxyPerformance = new Proxy(_performance, {
get(target, prop) {
if (prop === 'now') {
return function () {
console.log('[performance.now]');
return target.now();
};
}
return Reflect.get(target, prop);
}
});
// 注意:window.performance 本身如果 configurable:false 则这一步也做不到
Object.defineProperty(window, 'performance', {
get: () => proxyPerformance
});
踩坑
freeze只是浅冻结,obj.nested.key = 1仍然可以修改- 冻结后既不能 Hook 属性,也不能 delete,逆向时遇到冻结对象只能 Proxy 整体替换
Object.seal / Object.isSealed
seal 比 freeze 弱一档:不可新增/删除属性,但已有属性的值仍然可以修改(writable 不变,configurable 全设为 false)。
Object.seal(obj)
Object.isSealed(obj)
Hook 场景:seal 后的对象可以修改属性值但不能 defineProperty
const config = { mode: 'normal' };
Object.seal(config);
config.mode = 'hacked'; // 允许,seal 不改变 writable
// Object.defineProperty(config, 'mode', { get: ()=>'hacked' }); // 抛错,configurable:false
逆向时如果遇到 sealed 对象要 Hook,同样只能用 Proxy 整体替换。
Object.preventExtensions / Object.isExtensible
最弱的限制:只阻止新增属性,已有属性的 configurable/writable 不变。
Object.preventExtensions(obj)
Object.isExtensible(obj) // 默认 true,preventExtensions 后变 false
Hook 场景:检测对象是否可以新增属性(决定是否可直接挂载 Hook)
if (!Object.isExtensible(window)) {
// window 被锁定,不能新增属性(极少见)
console.warn('window 不可扩展');
}
三种限制的对比:
| 方法 | 新增属性 | 删除属性 | 修改值 | 修改描述符 |
|---|---|---|---|---|
preventExtensions |
禁止 | 允许 | 允许 | 允许 |
seal |
禁止 | 禁止 | 允许 | 禁止 |
freeze |
禁止 | 禁止 | 禁止 | 禁止 |
Object.getOwnPropertyNames
返回对象自身所有属性名(包含不可枚举属性),不包含 Symbol 属性。
Object.getOwnPropertyNames(obj)
// 对比:Object.keys(obj) 只返回可枚举属性
Hook 场景 1:找出对象上所有隐藏的不可枚举属性(侦察目标对象结构)
// Object.keys 看不到不可枚举属性,getOwnPropertyNames 全部暴露
const hidden = Object.getOwnPropertyNames(window).filter(
key => !Object.prototype.propertyIsEnumerable.call(window, key)
);
console.log('不可枚举属性:', hidden);
Hook 场景 2:遍历原型链上的所有方法,批量 Hook
function getAllMethodNames(obj) {
const names = new Set();
let proto = obj;
while (proto && proto !== Object.prototype) {
Object.getOwnPropertyNames(proto)
.filter(n => typeof proto[n] === 'function')
.forEach(n => names.add(n));
proto = Object.getPrototypeOf(proto);
}
return [...names];
}
// 获取 XMLHttpRequest 原型链上所有方法名
getAllMethodNames(XMLHttpRequest.prototype);
// ['open', 'send', 'abort', 'setRequestHeader', ...]
Hook 场景 3:检测原型上是否存在可疑方法(反调试侦察)
// 检测 Array.prototype 上是否有被注入的方法
const nativeMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'map', 'filter', 'reduce'];
Object.getOwnPropertyNames(Array.prototype).forEach(name => {
if (!nativeMethods.includes(name)) {
console.warn('[可疑] Array.prototype 上发现非原生方法:', name);
}
});
Object.getOwnPropertySymbols
返回对象自身所有 Symbol 属性键,普通枚举完全无法发现这些属性。
Object.getOwnPropertySymbols(obj)
Hook 场景 1:找出用 Symbol 隐藏的私有状态
// 某些库用 Symbol 存储内部状态,外部无法直接访问
const syms = Object.getOwnPropertySymbols(targetObj);
syms.forEach(sym => {
console.log(sym.toString(), targetObj[sym]);
});
// 可能发现:Symbol(kState), Symbol(fetch state) 等内部属性
Hook 场景 2:Hook Symbol.toPrimitive(影响类型转换)
// Symbol.toPrimitive 控制对象被转换为原始值时的行为
const originalFn = targetObj[Symbol.toPrimitive];
targetObj[Symbol.toPrimitive] = function (hint) {
console.log('[toPrimitive] hint =', hint);
return originalFn ? originalFn.call(this, hint) : String(this);
};
Hook 场景 3:Hook Symbol.iterator(拦截 for...of / 解构)
const _iter = Array.prototype[Symbol.iterator];
Array.prototype[Symbol.iterator] = function () {
console.log('[Array iterator] 被调用,length =', this.length);
return _iter.call(this);
};
Object.assign
将一个或多个源对象的可枚举自身属性浅拷贝到目标对象。
Object.assign(target, ...sources)
Hook 场景 1:快速拷贝原始方法备份
// 保存一批原生方法的引用,防止被污染后丢失
const nativeMethods = Object.assign(Object.create(null), {
defineProperty: Object.defineProperty.bind(Object),
getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor.bind(Object),
freeze: Object.freeze.bind(Object)
});
// 即使后续 Object.defineProperty 被 Hook,依然可以用 nativeMethods.defineProperty
Hook 场景 2:合并 Hook 配置
const defaultConfig = { logGet: true, logSet: true, logCall: false };
const userConfig = { logSet: false, logCall: true };
const config = Object.assign({}, defaultConfig, userConfig);
// { logGet: true, logSet: false, logCall: true }
踩坑
Object.assign 无法复制 getter/setter,只会把 getter 的返回值当成普通值复制过去:
const src = { get foo() { return 'bar'; } };
const dst = Object.assign({}, src);
// dst.foo === 'bar'(普通值),getter 丢失了
// 需要用 Object.getOwnPropertyDescriptors 才能保留 getter/setter
Object.keys / Object.values / Object.entries
只返回对象自身的可枚举属性(不包括原型链,不包括 Symbol)。
Object.keys(obj) // 返回属性名数组
Object.values(obj) // 返回属性值数组
Object.entries(obj) // 返回 [key, value] 对数组
Hook 场景 1:遍历请求参数对象,找出签名字段
// 拦截 fetch 后解析请求体,找出 sign / token 字段
window.fetch = async function (url, options) {
if (options?.body) {
try {
const body = JSON.parse(options.body);
Object.entries(body).forEach(([k, v]) => {
if (['sign', 'token', '_signature', 'nonce'].includes(k)) {
console.log(`[参数签名] ${k} = ${v}`);
debugger;
}
});
} catch {}
}
return originalFetch(url, options);
};
Hook 场景 2:批量 Hook 某个命名空间下所有方法
// 将 CryptoJS 命名空间下所有加密方法全部 Hook
Object.entries(CryptoJS).forEach(([name, algo]) => {
if (algo && typeof algo.encrypt === 'function') {
const _enc = algo.encrypt.bind(algo);
algo.encrypt = function (...args) {
console.log(`[CryptoJS.${name}.encrypt]`, args);
return _enc(...args);
};
}
});
Hook 场景 3:检测 window 上被注入的可疑全局变量
// 记录页面加载前 window 的 key 集合
const beforeKeys = new Set(Object.keys(window));
// 等待页面脚本执行后
setTimeout(() => {
const afterKeys = new Set(Object.keys(window));
const newKeys = [...afterKeys].filter(k => !beforeKeys.has(k));
console.log('[新增全局变量]', newKeys);
}, 3000);
Object.hasOwn
Object.hasOwn(obj, prop) // ES2022,替代 obj.hasOwnProperty(prop)
hasOwnProperty 存在被原型污染的风险(如果对象是 Object.create(null) 的,它根本没有这个方法)。Object.hasOwn 是静态方法,更安全。
Hook 场景:安全检测某个属性是否是对象自有属性
// 不推荐
if (obj.hasOwnProperty('sign')) { ... } // 若 obj 原型被污染则出错
// 推荐
if (Object.hasOwn(obj, 'sign')) { ... }
// 在 Hook 代码中判断请求参数是否包含签名字段
function hasSignField(params) {
return ['sign', 'token', '_ts', 'nonce'].some(k => Object.hasOwn(params, k));
}
Object.fromEntries
将 [key, value] 对的可迭代对象(如 Map、entries() 结果)转回对象。
Object.fromEntries(entries)
Hook 场景 1:修改请求参数后重新组装
window.fetch = async function (url, options) {
if (typeof url === 'string' && url.includes('/api/')) {
// 解析并修改 URL 查询参数
const urlObj = new URL(url);
const params = Object.fromEntries(urlObj.searchParams.entries());
console.log('[fetch params]', params);
// 修改某个参数(如替换 timestamp 为固定值方便重放)
params.ts = '1700000000';
const newSearch = new URLSearchParams(params).toString();
url = `${urlObj.origin}${urlObj.pathname}?${newSearch}`;
}
return originalFetch(url, options);
};
Hook 场景 2:将 Map 形式的 Header 转为普通对象打印
XMLHttpRequest.prototype.send = function (body) {
// 将 Headers 对象转为普通对象,方便打印
if (this._headers) {
const headersObj = Object.fromEntries(
Object.entries(this._headers)
);
console.log('[XHR headers]', headersObj);
}
return _send.call(this, body);
};
综合场景:检测与反检测对抗
场景 A:检测 window 属性是否被 Hook(反向侦察)
网站用来检测逆向者是否注入了 Hook:
// 网站侧:检测 XMLHttpRequest.prototype.open 是否被替换
function detectHook(obj, method) {
const desc = Object.getOwnPropertyDescriptor(obj, method);
if (!desc) return false;
const fn = desc.value || desc.get?.();
if (!fn) return false;
// 检测函数 toString 是否包含 native code
return !/\[native code\]/.test(fn.toString());
}
if (detectHook(XMLHttpRequest.prototype, 'open')) {
console.warn('open 被 Hook 了!');
}
逆向者的反制——让 toString 返回原生字符串:
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) {
console.log('[XHR open]', args[1]);
return _open.apply(this, args);
};
// 关键:伪造 toString
Object.defineProperty(XMLHttpRequest.prototype.open, 'toString', {
value: () => 'function open() { [native code] }',
configurable: false // 锁死,让别人也无法再改
});
场景 B:保存一份干净的 Object 方法引用(防止被污染)
// 在所有脚本执行之前,保存原生方法引用
// 即使后续 Object.defineProperty 被 Hook,这些引用依然指向原生实现
const safeDefineProperty = Object.defineProperty.bind(Object);
const safeGetDescriptor = Object.getOwnPropertyDescriptor.bind(Object);
const safeGetProto = Object.getPrototypeOf.bind(Object);
const safeFreeze = Object.freeze.bind(Object);
const safeGetOwnPropNames = Object.getOwnPropertyNames.bind(Object);
// 后续 Hook 操作全部使用 safe 前缀的版本
safeDefineProperty(window, 'myHook', { value: true, configurable: false });
场景 C:检测 defineProperty 本身是否被 Hook(深度防御)
// 如果连 Object.defineProperty 都被 Hook 了,上面所有操作都不可信
function isDefinePropertyHooked() {
return !/\[native code\]/.test(Object.defineProperty.toString());
}
if (isDefinePropertyHooked()) {
// 尝试从 iframe 中取干净的原生方法
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const cleanDefineProperty = iframe.contentWindow.Object.defineProperty;
document.body.removeChild(iframe);
// 用 cleanDefineProperty 注入 Hook
}
场景 D:完整的 Cookie Hook 最佳实践
综合运用上述方法,写出最稳定的 Cookie Hook:
(function () {
// 1. 保存原生方法引用(防止递归/被污染)
const _defineProperty = Object.defineProperty.bind(Object);
const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor.bind(Object);
// 2. 从 Document.prototype 取原生访问器(不能从 document 实例取,否则触发自己)
const cookieDesc = _getOwnPropertyDescriptor(Document.prototype, 'cookie');
if (!cookieDesc || !cookieDesc.configurable) {
console.warn('[Cookie Hook] 无法注入,cookie 描述符不可配置');
return;
}
const nativeGet = cookieDesc.get;
const nativeSet = cookieDesc.set;
// 3. 注入 Hook
_defineProperty(document, 'cookie', {
configurable: true,
enumerable: true,
get() {
return nativeGet.call(document);
},
set(v) {
// 4. 条件断点:只在写入目标 cookie 时断下
if (v.startsWith('_acl') || v.includes('sign=')) {
console.log('[Cookie SET 命中]', v, new Error().stack);
debugger;
}
nativeSet.call(document, v);
}
});
console.log('[Cookie Hook] 注入成功');
})();
最佳实践
优先使用 Object.defineProperty 做精确属性 Hook:Hook 单个属性时,Object.defineProperty 可以设置 get/set 拦截器,并控制 configurable/enumerable/writable,比直接赋值更细粒度,也更难被检测到。
用 Object.getOwnPropertyDescriptor 在 Hook 前保存原始描述符:
const orig = Object.getOwnPropertyDescriptor(navigator, 'userAgent')
Object.defineProperty(navigator, 'userAgent', {
get: () => { console.log('[UA accessed]'); return orig.get.call(this) }
})
先保存再 Hook,既能监控访问又保留原始行为,避免 Hook 后功能异常。
Object.freeze 保护 Hook 代码自身:注入 Hook 后,用 Object.freeze(hookedObject) 防止目标代码用 delete/赋值覆盖 Hook;Object.isFrozen 检测是否已被保护。
Object.getPrototypeOf 追踪原型链:混淆代码有时将方法定义在原型上而非实例上,用 Object.getPrototypeOf(obj) 找到实际原型,然后 Hook 原型上的方法,比 Hook 实例属性影响范围更广。
Object.entries + Object.fromEntries 做深拷贝前的检测:检测目标对象有哪些可枚举属性时,Object.entries(obj) 比 for...in 更安全(不包含原型链属性);Object.fromEntries 可以快速重建过滤后的对象。
常见陷阱
陷阱:Object.freeze 后 Hook 失效
现象: 尝试 Object.defineProperty Hook 某属性,但静默失败(严格模式下抛 TypeError)。
原因: 目标对象或其原型已被 Object.freeze() 保护,被冻结后所有属性修改都被阻止。
解决: 检查 Object.isFrozen(obj);若已冻结,需要替换整个对象引用(window.targetObj = new Proxy(originalObj, {...}))而非修改已冻结对象的属性。
陷阱:configurable: false 的属性无法用 defineProperty 重定义
现象: 对某属性 defineProperty 抛出 TypeError: Cannot redefine property。
原因: 属性描述符中 configurable: false 禁止重新定义;原生浏览器 API(如 navigator.userAgent)通常都是 configurable: false。
解决: 先用 Object.defineProperty 替换父对象(如整个 navigator);或使用 Proxy 代理父对象而非直接修改属性。
陷阱:Object.assign 只做浅拷贝,深层属性 Hook 失效
现象: 用 Object.assign 创建目标对象副本后再 Hook,但原始对象的深层属性访问没被拦截。
原因: Object.assign 只复制第一层可枚举属性,嵌套对象还是同一引用;Hook 了副本的属性,不影响原始对象的访问路径。
解决: Hook 需要在原始对象引用上操作;或用深拷贝(JSON.parse(JSON.stringify(obj)))但注意会丢失函数属性;或直接 Hook 原始对象。