代理脚本——爬虫
1. 核心代理机制
代码的核心是使用 JavaScript 的 Proxy 对象来拦截并记录对象的属性访问和修改操作。Proxy 是 ES6 引入的特性,允许你拦截并重新定义对象的基本操作。
return new Proxy(target, {// 拦截属性读取操作get(target, property, receiver) {// 记录读取操作的日志console.log("方法:", "get", "对象:", objectName, "属性:", property, // ... 其他日志信息);return Reflect.get(target, property, receiver);},// 拦截属性设置操作set(target, property, value, receiver) {// 记录旧值const oldValue = target[property];let result;let failureReason = '';try {// 尝试设置属性值result = Reflect.set(target, property, value, receiver);// 分析设置失败的原因if (!result) {failureReason = analyzeFailure(target, property, value);}} catch (error) {result = false;failureReason = error.message;}// 记录设置操作的日志console.log("方法:", "set", "对象:", objectName, "属性:", property, "旧值:", getSafeValueString(oldValue),// ... 其他日志信息"设置结果:", result? "✅成功" : "❌失败",result? "" : ` 原因: ${failureReason}`);return result;}
});
2. 安全值转换
getSafeValueString
函数处理各种类型的值,确保它们能以安全的方式转换为字符串:
function getSafeValueString(value) {try {// 处理基本类型和nullif (typeof value!== 'object' || value === null) {return String(value);}// 处理DOM元素if (value instanceof HTMLElement) {return `<${value.tagName.toLowerCase()}> (${value.id || 'no id'})`;}// 处理数组if (Array.isArray(value)) {return `Array(${value.length})`;}// 处理普通对象return JSON.stringify(value, null, 2);} catch (error) {// 处理循环引用或其他复杂对象return '[Circular or Non-Serializable Object]';}
}
3. 设置失败分析
analyzeFailure
函数详细分析属性设置失败的原因:
function analyzeFailure(target, property, value) {const descriptor = Object.getOwnPropertyDescriptor(target, property) || {};const propertyExists = descriptor.value!== undefined || descriptor.get!== undefined;const isReadOnly = descriptor.writable === false;const isGetterOnly = descriptor.get!== undefined && descriptor.set === undefined;const isObjectFrozen = Object.isFrozen(target);const isPropertyFrozen = propertyExists && Object.isExtensible(target) && Object.isFrozen(Object.getOwnPropertyDescriptor(target, property));// 针对特定DOM属性的类型检查let typeMismatch = false;let typeErrorMessage = '';if (objectName === 'document' && property === 'title') {typeMismatch = typeof value!=='string';typeErrorMessage = 'document.title必须是字符串类型';} else if (objectName === 'location' && property === 'href') {typeMismatch = typeof value!=='string';typeErrorMessage = 'location.href必须是字符串类型';}// ... 其他类型检查// 根据不同情况返回失败原因if (!propertyExists && isObjectFrozen) {return '对象被冻结,无法添加新属性';} else if (isReadOnly) {return '属性是只读的';}// ... 其他失败原因
}
4. 环境设置与测试
代码模拟了浏览器环境,并为常用对象设置代理:
// 模拟浏览器环境
window = {document: { title: '默认标题' },location: { href: 'http://example.com' },// ... 其他对象
};// 将window添加到全局对象
global.window = window;// 为指定对象设置代理
setupEnvironmentLogging(['window', 'document', 'location', 'navigator', 'history', 'screen'
]);// 测试操作
window.document.title = '新标题';
console.log("window.location.href:"+window.location.href);
5. 日志输出示例
当执行window.document.title = '新标题'
时,控制台会输出:
方法: get 对象: window 属性: document 属性类型: string 属性值: Object({...}) 属性值类型: object
方法: get 对象: document 属性: title 属性类型: string 属性值: "默认标题" 属性值类型: string
方法: set 对象: document 属性: title 属性类型: string 旧值: "默认标题" 旧值类型: string 新值: "新标题" 新值类型: string 设置结果: ✅成功
6. 实际应用场景
这个工具在以下场景特别有用:
-
调试复杂应用:当你需要追踪某个属性何时被修改或访问时。
-
学习 JavaScript:通过观察对象属性的访问模式,深入理解 JavaScript 的工作原理。
-
性能优化:找出频繁访问的热点属性,优化代码结构。
-
安全审计:检测并记录对敏感对象的非法访问或修改。
-
框架开发:在开发 JavaScript 库或框架时,用于监控内部状态变化。
7. 注意事项
- 代理会影响性能,不建议在生产环境中使用。
- 循环引用和复杂对象(如 DOM 元素)需要特殊处理,这就是
getSafeValueString
函数的作用。 - 某些内置对象(如 location)可能有特殊的安全限制,即使使用 Proxy 也无法修改。
这个工具提供了一种强大的方式来观察和理解 JavaScript 对象的行为,特别是在浏览器环境中。通过记录详细的访问和修改日志,开发者可以更高效地调试和优化代码。
完整代码
/*** 创建一个用于记录对象属性访问和修改的代理* @param {Object} target - 要代理的目标对象* @param {string} objectName - 目标对象的名称,用于日志输出* @returns {Proxy} - 返回代理后的对象*/
function createLoggingProxy(target, objectName) {/*** 安全地将值转换为字符串表示形式* 处理特殊情况,如DOM元素、循环引用和复杂对象* @param {any} value - 要转换的值* @returns {string} - 值的字符串表示*/function getSafeValueString(value) {try {// 处理基本类型和nullif (typeof value!== 'object' || value === null) {return String(value);}// 处理DOM元素,返回标签名和ID信息if (value instanceof HTMLElement) {return `<${value.tagName.toLowerCase()}> (${value.id || 'no id'})`;}// 处理数组,返回数组长度信息if (Array.isArray(value)) {return `Array(${value.length})`;}// 处理普通对象,返回格式化的JSON字符串return JSON.stringify(value, null, 2);} catch (error) {// 处理循环引用或其他无法序列化的对象return '[Circular or Non-Serializable Object]';}}/*** 分析属性设置失败的原因* @param {Object} target - 目标对象* @param {string} property - 属性名* @param {any} value - 要设置的值* @returns {string} - 失败原因描述*/function analyzeFailure(target, property, value) {// 获取属性描述符,如果属性不存在则返回空对象const descriptor = Object.getOwnPropertyDescriptor(target, property) || {};// 检查属性是否存在(通过值或getter判断)const propertyExists = descriptor.value!== undefined || descriptor.get!== undefined;// 检查属性是否为只读(writable为false)const isReadOnly = descriptor.writable === false;// 检查属性是否只有getter没有setterconst isGetterOnly = descriptor.get!== undefined && descriptor.set === undefined;// 检查整个对象是否被冻结(不可扩展且所有属性不可配置)const isObjectFrozen = Object.isFrozen(target);// 检查属性描述符是否被冻结(不可修改)const isPropertyFrozen = propertyExists && Object.isExtensible(target) && Object.isFrozen(Object.getOwnPropertyDescriptor(target, property));// 检查值类型是否与属性期望类型不匹配let typeMismatch = false;let typeErrorMessage = '';// 针对常见DOM属性的类型检查(非详尽,可根据需要扩展)if (objectName === 'document' && property === 'title') {typeMismatch = typeof value!=='string';typeErrorMessage = 'document.title必须是字符串类型';} else if (objectName === 'location' && property === 'href') {typeMismatch = typeof value!=='string';typeErrorMessage = 'location.href必须是字符串类型';} else if (objectName ==='screen' && (property === 'width' || property === 'height')) {typeMismatch = typeof value!== 'number';typeErrorMessage = `${objectName}.${property}必须是数字类型`;}// 根据不同情况返回具体的失败原因if (!propertyExists && isObjectFrozen) {return '对象被冻结,无法添加新属性';} else if (isReadOnly) {return '属性是只读的';} else if (isGetterOnly) {return '属性是getter-only,没有setter';} else if (isPropertyFrozen) {return '属性被冻结,无法修改';} else if (typeMismatch) {return typeErrorMessage;} else {return '未知原因(可能是内部限制或安全策略阻止)';}}return new Proxy(target, {// 拦截属性读取操作get(target, property, receiver) {const value = Reflect.get(target, property, receiver);console.log("方法:", "get", "对象:", objectName, "属性:", property, "属性类型:", typeof property, "属性值:", getSafeValueString(value),"属性值类型:", typeof value);return value;},// 拦截属性设置操作set(target, property, value, receiver) {const oldValue = target[property];let result; // 存储设置操作的结果let failureReason = ''; // 存储失败原因try {// 尝试设置属性值并获取结果result = Reflect.set(target, property, value, receiver);// 如果设置失败,分析失败原因if (!result) {failureReason = analyzeFailure(target, property, value);}} catch (error) {// 捕获设置过程中可能发生的异常result = false;failureReason = error.message;console.error(`设置 ${objectName}.${property} 时出错:`, error);}console.log("方法:", "set", "对象:", objectName, "属性:", property, "属性类型:", typeof property, "旧值:", getSafeValueString(oldValue),"旧值类型:", typeof oldValue,"新值:", getSafeValueString(value),"新值类型:", typeof value,"设置结果:", result? "✅成功" : "❌失败",result? "" : ` 原因: ${failureReason}`);return result;}});
}/*** 为环境对象数组应用日志代理* @param {string[]} objectNames - 要代理的对象名称数组*/
function setupEnvironmentLogging(objectNames) {for (const name of objectNames) {try {// 检查对象是否存在if (typeof window[name]!== 'undefined') {// 使用window[name]获取实际对象并应用代理window[name] = createLoggingProxy(window[name], name);console.log(`成功为 ${name} 设置日志代理`);} else {console.warn(`对象 ${name} 不存在,跳过代理设置`);}} catch (error) {console.error(`设置 ${name} 的代理时出错:`, error);}}
}// 先定义window对象
window = {window: {}, // 确保window.window存在document: {title: '默认标题',createElement: function() { return {}; }},location: {href: 'http://example.com'},navigator: {userAgent: 'Node.js模拟'},history: {length: 0},screen: {width: 1920,height: 1080}
};// 然后将window添加到全局对象
global.window = window;// 应用代理到常用浏览器环境对象
setupEnvironmentLogging(['window', 'document', 'location', 'navigator', 'history', 'screen'
]);// 测试一些操作
console.log("测试:")
window.document.title = '新标题';
console.log("window.location.href:"+window.location.href);