代理核心功能 - createLoggingProxy 函数
/**
* 创建一个用于记录对象属性访问和修改的代理
* @param {Object} target - 要代理的目标对象
* @param {string} objectName - 目标对象的名称,用于日志输出
* @returns {Proxy} - 返回代理后的对象
*/
function createLoggingProxy(target, objectName) {
这是创建代理的核心函数,它接收两个参数:target
是要代理的实际对象,objectName
是为了日志可读性而设置的对象名称。
/**
* 安全地将值转换为字符串表示形式
* 处理特殊情况,如DOM元素、循环引用和复杂对象
* @param {any} value - 要转换的值
* @returns {string} - 值的字符串表示
*/
function getSafeValueString(value) {
这个辅助函数用于将任意值安全地转换为字符串,处理各种特殊情况以避免错误。
try {
// 处理基本类型和null
if (typeof value!== 'object' || value === null) {
return String(value);
}
首先检查是否为基本类型(如字符串、数字、布尔值)或 null,如果是则直接转换为字符串。
// 处理DOM元素,返回标签名和ID信息
if (value instanceof HTMLElement) {
return `<${value.tagName.toLowerCase()}> (${value.id || 'no id'})`;
}
如果是 DOM 元素,则返回其标签名和 ID 信息,例如 <div> (main-content)
。
// 处理数组,返回数组长度信息
if (Array.isArray(value)) {
return `Array(${value.length})`;
}
如果是数组,则返回数组的长度信息,例如 Array(5)
。
// 处理普通对象,返回格式化的JSON字符串
return JSON.stringify(value, null, 2);
} catch (error) {
// 处理循环引用或其他无法序列化的对象
return '[Circular or Non-Serializable Object]';
}
}
对于普通对象,尝试使用 JSON.stringify 转换为格式化的字符串。如果遇到循环引用或其他无法序列化的情况,捕获错误并返回特定提示。
/**
* 分析属性设置失败的原因
* @param {Object} target - 目标对象
* @param {string} property - 属性名
* @param {any} value - 要设置的值
* @returns {string} - 失败原因描述
*/
function analyzeFailure(target, property, value) {
// 获取属性描述符,如果属性不存在则返回空对象
const descriptor = Object.getOwnPropertyDescriptor(target, property) || {};
获取目标对象的属性描述符,用于分析属性的特性(如是否只读、是否有 setter 等)。
// 检查属性是否存在(通过值或getter判断)
const propertyExists = descriptor.value!== undefined || descriptor.get!== undefined;
// 检查属性是否为只读(writable为false)
const isReadOnly = descriptor.writable === false;
// 检查属性是否只有getter没有setter
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));
依次检查各种可能导致属性设置失败的原因:属性是否存在、是否只读、是否只有 getter、对象是否被冻结、属性描述符是否被冻结。
// 检查值类型是否与属性期望类型不匹配
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}必须是数字类型`;
}
检查值的类型是否符合特定属性的期望类型,针对常见的 DOM 属性进行了类型检查。
// 根据不同情况返回具体的失败原因
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;
},
创建并返回一个 Proxy 对象,拦截属性读取操作。使用 Reflect.get 获取实际值,并记录详细的读取信息,包括方法名、对象名、属性名、属性类型、属性值及其类型。
// 拦截属性设置操作
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);
}
拦截属性设置操作,记录旧值,尝试设置新值,并捕获可能的异常。如果设置失败,调用 analyzeFailure 分析原因。
console.log(
"方法:", "set",
"对象:", objectName,
"属性:", property,
"属性类型:", typeof property,
"旧值:", getSafeValueString(oldValue),
"旧值类型:", typeof oldValue,
"新值:", getSafeValueString(value),
"新值类型:", typeof value,
"设置结果:", result? "✅成功" : "❌失败",
result? "" : ` 原因: ${failureReason}`
);
return result;
}
});
}
记录详细的设置信息,包括方法名、对象名、属性名、属性类型、旧值及其类型、新值及其类型、设置结果和失败原因(如果有)。最后返回设置结果。
对象路径处理 - setupEnvironmentLogging 函数
/**
* 为环境对象应用日志代理
* @param {string} objectPath - 要代理的对象路径,如 "document" 或 "document.documentElement"
*/
function setupEnvironmentLogging(objectPath) {
try {
// 解析对象路径
const pathParts = objectPath.split('.');
let currentObject = window;
let parentObject = null;
let propertyName = null;
这个函数用于为指定路径的对象设置日志代理。首先将对象路径拆分为多个部分,并初始化一些变量用于遍历路径。
// 遍历路径,找到目标对象及其父对象
for (let i = 0; i < pathParts.length; i++) {
propertyName = pathParts[i];
if (currentObject === undefined || currentObject === null) {
console.warn(`对象路径 ${objectPath} 中的 ${pathParts.slice(0, i + 1).join('.')} 不存在,跳过代理设置`);
return;
}
parentObject = currentObject;
currentObject = currentObject[propertyName];
}
遍历路径的每个部分,逐级查找对象。如果在路径中遇到不存在的对象,记录警告并退出。
// 检查对象是否存在
if (currentObject === undefined || currentObject === null) {
console.warn(`对象 ${objectPath} 不存在,跳过代理设置`);
return;
}
// 创建代理
const proxy = createLoggingProxy(currentObject, objectPath);
// 设置代理到父对象的属性
if (parentObject) {
parentObject[propertyName] = proxy;
console.log(`成功为 ${objectPath} 设置日志代理`);
} else {
console.error(`无法设置 ${objectPath} 的代理:找不到父对象`);
}
} catch (error) {
console.error(`设置 ${objectPath} 的代理时出错:`, error);
}
}
检查最终找到的对象是否存在,存在则创建代理,并将代理设置到父对象的相应属性上。捕获并处理可能的异常。
环境模拟与测试
// 定义window对象
// 补环境
window = {
window: {}, // 确保window.window存在
document: {
createElement: function(tagName) {
console.log("document ==========> createElement:", tagName);
},
documentElement: {
},
},
HTMLDocument: {},
Element: {},
HTMLElement: {},
location: {},
navigator: {},
history: {},
screen: {},
};
// 然后将window添加到全局对象
global.window = window;
模拟浏览器环境中的 window 对象及其属性,为测试提供基础环境。
// 应用代理到常用浏览器环境对象
setupEnvironmentLogging('window');
setupEnvironmentLogging('document');
setupEnvironmentLogging('document.documentElement');
setupEnvironmentLogging('HTMLDocument');
setupEnvironmentLogging('Element');
setupEnvironmentLogging('HTMLElement');
setupEnvironmentLogging('location');
setupEnvironmentLogging('navigator');
setupEnvironmentLogging('history');
setupEnvironmentLogging('screen');
为多个常见的浏览器环境对象设置日志代理。
// 测试一些操作
console.log("\n\n\n");
console.log("代理脚本测试:");
window.document.title = '新标题';
console.log("window.location.href:" + window.location.href);
console.log("代理脚本测试成功!!!");
console.log("\n\n\n");
执行一些测试操作,验证代理是否正常工作。这些操作会触发代理的拦截器,从而记录相应的日志信息。
完整代码
/**
* 创建一个用于记录对象属性访问和修改的代理
* @param {Object} target - 要代理的目标对象
* @param {string} objectName - 目标对象的名称,用于日志输出
* @returns {Proxy} - 返回代理后的对象
*/
function createLoggingProxy(target, objectName) {
/**
* 安全地将值转换为字符串表示形式
* 处理特殊情况,如DOM元素、循环引用和复杂对象
* @param {any} value - 要转换的值
* @returns {string} - 值的字符串表示
*/
function getSafeValueString(value) {
try {
// 处理基本类型和null
if (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没有setter
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));
// 检查值类型是否与属性期望类型不匹配
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} objectPath - 要代理的对象路径,如 "document" 或 "document.documentElement"
*/
function setupEnvironmentLogging(objectPath) {
try {
// 解析对象路径
const pathParts = objectPath.split('.');
let currentObject = window;
let parentObject = null;
let propertyName = null;
// 遍历路径,找到目标对象及其父对象
for (let i = 0; i < pathParts.length; i++) {
propertyName = pathParts[i];
if (currentObject === undefined || currentObject === null) {
console.warn(`对象路径 ${objectPath} 中的 ${pathParts.slice(0, i + 1).join('.')} 不存在,跳过代理设置`);
return;
}
parentObject = currentObject;
currentObject = currentObject[propertyName];
}
// 检查对象是否存在
if (currentObject === undefined || currentObject === null) {
console.warn(`对象 ${objectPath} 不存在,跳过代理设置`);
return;
}
// 创建代理
const proxy = createLoggingProxy(currentObject, objectPath);
// 设置代理到父对象的属性
if (parentObject) {
parentObject[propertyName] = proxy;
console.log(`成功为 ${objectPath} 设置日志代理`);
} else {
console.error(`无法设置 ${objectPath} 的代理:找不到父对象`);
}
} catch (error) {
console.error(`设置 ${objectPath} 的代理时出错:`, error);
}
}
// 定义window对象
// 补环境
window = {
window: {}, // 确保window.window存在
document: {
createElement: function(tagName) {
console.log("document ==========> createElement:", tagName);
},
documentElement: {
},
},
HTMLDocument: {},
Element: {},
HTMLElement: {},
location: {},
navigator: {},
history: {},
screen: {},
};
// 然后将window添加到全局对象
global.window = window;
// 应用代理到常用浏览器环境对象
setupEnvironmentLogging('window');
setupEnvironmentLogging('document');
setupEnvironmentLogging('document.documentElement');
setupEnvironmentLogging('HTMLDocument');
setupEnvironmentLogging('Element');
setupEnvironmentLogging('HTMLElement');
setupEnvironmentLogging('location');
setupEnvironmentLogging('navigator');
setupEnvironmentLogging('history');
setupEnvironmentLogging('screen');
// 测试一些操作
console.log("\n\n\n");
console.log("代理脚本测试:");
window.document.title = '新标题';
console.log("window.location.href:" + window.location.href);
console.log("代理脚本测试成功!!!");
console.log("\n\n\n");
运行结果