ES6 Proxy 的 13 种拦截陷阱(Traps):如何利用它实现响应式与权限拦截

ES6 Proxy 的 13 种拦截陷阱(Traps):如何利用它实现响应式与权限拦截

欢迎各位来到今天的讲座,我们即将深入探讨 ES6 中一项强大而又巧妙的特性——Proxy。Proxy,顾名思义,是代理。它允许我们创建一个对象的代理,从而在对该对象进行各种操作(如读取属性、设置属性、调用方法等)时,插入自定义的逻辑。这就像在对象和其使用者之间设置了一个“守卫”,所有对对象的交互都必须先经过这个守卫的检查和处理。

这项元编程(meta-programming)能力为 JavaScript 带来了前所未有的灵活性,尤其在构建响应式系统、实现细粒度权限控制、数据验证、日志记录等高级场景中,展现出其核心价值。Vue 3 的响应式系统就是 Proxy 技术最著名的应用之一,它解决了 Vue 2 中 Object.defineProperty 存在的许多局限性。

我们将逐一剖析 Proxy 提供的 13 种“拦截陷阱”(Traps),理解它们各自的功能、参数,并通过具体的代码示例,演示如何在响应式数据和权限拦截两大核心场景中利用它们。

Proxy 的基本概念与结构

在深入陷阱之前,我们先回顾一下 Proxy 的基本构造:

const proxy = new Proxy(target, handler);
  • target:这是你想要代理的原始对象。它可以是任何类型的对象,包括函数、数组甚至是另一个 Proxy。
  • handler:这是一个对象,它包含了各种拦截陷阱。每一个陷阱都是一个函数,当对 proxy 执行某个操作时,如果 handler 中定义了相应的陷阱,该陷阱就会被调用。

handler 对象中的方法被称为“陷阱”,因为它们“捕获”了对 target 对象执行的基本操作。当一个操作被捕获时,陷阱函数可以执行自定义逻辑,然后决定是执行原始操作、修改操作结果,还是完全阻止操作。

为了确保代码的健壮性和未来的兼容性,强烈建议在陷阱内部通过 Reflect 对象来调用 target 上的原始操作。Reflect 是一个内置对象,提供了一组与 Proxy 陷阱方法同名的静态方法,它们的作用与默认的 target 操作行为一致。使用 Reflect 有以下几个优势:

  1. 一致性Reflect 方法的签名与 Proxy 陷阱的签名一致,使得代码更易读和维护。
  2. 默认行为Reflect 提供了 target 对象的默认行为,而无需手动实现或担心 this 绑定问题。
  3. 兼容性:即使 target 对象是不可扩展的或属性不可配置,Reflect 也能正确地执行操作。

现在,让我们逐一探索这 13 种拦截陷阱。

1. get(target, property, receiver)

get 陷阱用于拦截对象属性的读取操作。这包括访问属性(obj.prop)、方括号访问(obj['prop'])、以及通过原型链查找属性。

  • 参数

    • target:被代理的原始对象。
    • property:被访问的属性名(字符串或 Symbol)。
    • receiver:Proxy 或继承 Proxy 的对象,在属性访问中它就是 this 的值。
  • 返回值:可以是任何值,它将作为属性访问的结果返回。

  • 应用场景

    • 响应式系统:在属性被读取时,记录当前“作用域”(例如,一个渲染函数)对该属性的依赖。
    • 默认值:当访问一个不存在的属性时,返回一个预设的默认值。
    • 权限拦截:根据用户角色或上下文,决定是否允许读取某个属性,或者在读取前进行数据脱敏。
    • 计算属性:模拟计算属性,当访问时动态计算其值。
  • 代码示例

// 响应式系统中的依赖追踪
const activeEffect = []; // 模拟一个全局的活跃副作用函数栈

function track(target, key) {
    if (activeEffect.length > 0) {
        const effect = activeEffect[activeEffect.length - 1];
        console.log(`[Reactive] Tracking dependency: ${key} for effect.`);
        // 实际场景中,这里会将effect与target[key]建立映射
        // 例如:depsMap.get(target).get(key).add(effect);
    }
}

const reactiveHandler = {
    get(target, property, receiver) {
        track(target, property); // 追踪依赖
        return Reflect.get(target, property, receiver);
    }
};

let user = { name: "Alice", age: 30 };
const reactiveUser = new Proxy(user, reactiveHandler);

// 权限拦截与默认值
const permissionHandler = {
    get(target, property, receiver) {
        // 模拟当前用户角色
        const currentUserRole = "guest"; // 可以是 'admin', 'user', 'guest'

        if (property === 'salary' && currentUserRole !== 'admin') {
            console.warn(`[Permission] Access denied to 'salary' for role: ${currentUserRole}`);
            return undefined; // 或者抛出错误
        }
        if (property === 'email' && !Reflect.has(target, property)) {
            console.log(`[Default] Providing default email for missing property.`);
            return '[email protected]';
        }
        console.log(`[Access] Reading property: ${String(property)}`);
        return Reflect.get(target, property, receiver);
    }
};

let employee = { name: "Bob", title: "Developer", salary: 70000 };
const securedEmployee = new Proxy(employee, permissionHandler);

activeEffect.push(() => {
    console.log(`User name is: ${reactiveUser.name}`);
});
reactiveUser.name; // 输出: [Reactive] Tracking dependency: name for effect.
activeEffect.pop();

console.log(securedEmployee.name);    // Output: [Access] Reading property: name, Bob
console.log(securedEmployee.salary);  // Output: [Permission] Access denied to 'salary' for role: guest, undefined
console.log(securedEmployee.email);   // Output: [Default] Providing default email for missing property., [email protected]

2. set(target, property, value, receiver)

set 陷阱用于拦截对象属性的赋值操作。

  • 参数

    • target:被代理的原始对象。
    • property:被设置的属性名。
    • value:要设置的新值。
    • receiver:Proxy 或继承 Proxy 的对象,在属性赋值中它就是 this 的值。
  • 返回值:必须返回一个布尔值。如果属性设置成功,返回 true;如果失败(例如,因为验证不通过),返回 false。在严格模式下,返回 false 会抛出 TypeError

  • 应用场景

    • 响应式系统:在属性被修改时,触发依赖于该属性的所有“作用域”进行更新。
    • 数据验证:在赋值前检查值的有效性(类型、范围等),如果无效则阻止赋值或抛出错误。
    • 数据转换:在赋值前对值进行格式化或转换。
    • 权限拦截:根据用户角色或上下文,决定是否允许修改某个属性。
    • 日志记录:记录属性的变更历史。
  • 代码示例

// 响应式系统中的触发更新
const effectsMap = new WeakMap();

function trigger(target, key) {
    if (effectsMap.has(target) && effectsMap.get(target).has(key)) {
        console.log(`[Reactive] Triggering effects for key: ${key}`);
        // 实际场景中,这里会遍历并执行所有依赖于target[key]的effect
        // effectsMap.get(target).get(key).forEach(effect => effect());
    }
}

const reactiveSetHandler = {
    set(target, property, value, receiver) {
        const oldValue = Reflect.get(target, property, receiver);
        if (oldValue === value) { // 值未改变,不触发更新
            return true;
        }

        console.log(`[Reactive] Setting property: ${String(property)} to ${value}`);
        const result = Reflect.set(target, property, value, receiver);
        if (result) {
            trigger(target, property); // 触发更新
        }
        return result;
    }
};

let data = { count: 0 };
const reactiveData = new Proxy(data, reactiveSetHandler);

// 模拟注册effect
if (!effectsMap.has(data)) effectsMap.set(data, new Map());
if (!effectsMap.get(data).has('count')) effectsMap.get(data).set('count', new Set());
effectsMap.get(data).get('count').add(() => console.log('Count changed!'));

reactiveData.count = 1; // Output: [Reactive] Setting property: count to 1, [Reactive] Triggering effects for key: count, Count changed!
reactiveData.count = 1; // No output for trigger, as value is same.
reactiveData.count = 2; // Output: [Reactive] Setting property: count to 2, [Reactive] Triggering effects for key: count, Count changed!

// 权限拦截与数据验证
const validationAndPermissionHandler = {
    set(target, property, value, receiver) {
        const currentUserRole = "user"; // 模拟当前用户角色

        if (property === 'price') {
            if (currentUserRole !== 'admin') {
                console.error(`[Permission] User role '${currentUserRole}' cannot change 'price'.`);
                return false; // 阻止赋值
            }
            if (typeof value !== 'number' || value <= 0) {
                console.error(`[Validation] Price must be a positive number.`);
                return false; // 阻止赋值
            }
        }
        if (property === 'name' && typeof value !== 'string') {
            console.error(`[Validation] Name must be a string.`);
            return false;
        }

        console.log(`[Set] Setting property ${String(property)} to ${value}`);
        return Reflect.set(target, property, value, receiver);
    }
};

let product = { name: "Laptop", price: 1200 };
const securedProduct = new Proxy(product, validationAndPermissionHandler);

securedProduct.name = "Gaming Laptop"; // Output: [Set] Setting property name to Gaming Laptop
console.log(securedProduct.name);    // Output: Gaming Laptop

securedProduct.price = 1500; // Output: [Permission] User role 'user' cannot change 'price'. (returns false)
console.log(securedProduct.price);   // Output: 1200 (unchanged)

// 切换到 admin 角色,并尝试无效值
// const currentUserRole = "admin"; // 假设角色切换
// securedProduct.price = "invalid"; // Output: [Validation] Price must be a positive number.
// securedProduct.price = -100;    // Output: [Validation] Price must be a positive number.
// securedProduct.price = 1500;    // Output: [Set] Setting property price to 1500
// console.log(securedProduct.price); // Output: 1500

3. has(target, property)

has 陷阱用于拦截 in 操作符。

  • 参数

    • target:被代理的原始对象。
    • property:要检查的属性名。
  • 返回值:必须返回一个布尔值。

  • 应用场景

    • 隐藏私有属性:让某些属性在 in 检查时表现为不存在。
    • 条件属性存在:根据特定条件决定属性是否“存在”。
    • 权限拦截:控制用户是否能发现某个属性的存在。
  • 代码示例

const hiddenPropertyHandler = {
    has(target, property) {
        if (property.startsWith('_')) {
            console.log(`[Has] Hiding private property: ${String(property)}`);
            return false; // 隐藏以 '_' 开头的属性
        }
        console.log(`[Has] Checking property: ${String(property)}`);
        return Reflect.has(target, property);
    }
};

let config = { id: 1, name: "AppConfig", _secret: "super_secret" };
const securedConfig = new Proxy(config, hiddenPropertyHandler);

console.log('id' in securedConfig);      // Output: [Has] Checking property: id, true
console.log('_secret' in securedConfig); // Output: [Has] Hiding private property: _secret, false
console.log('version' in securedConfig); // Output: [Has] Checking property: version, false

4. deleteProperty(target, property)

deleteProperty 陷阱用于拦截 delete 操作符。

  • 参数

    • target:被代理的原始对象。
    • property:要删除的属性名。
  • 返回值:必须返回一个布尔值。如果属性删除成功,返回 true;如果失败,返回 false。在严格模式下,返回 false 会抛出 TypeError

  • 应用场景

    • 防止删除:阻止某些关键属性被删除。
    • 权限拦截:根据用户角色决定是否允许删除属性。
    • 日志记录:记录属性的删除操作。
    • 级联删除:当删除某个属性时,自动删除与之相关的其他属性。
  • 代码示例

const deleteGuardHandler = {
    deleteProperty(target, property) {
        if (property === 'id') {
            console.warn(`[Delete] Cannot delete 'id' property.`);
            return false; // 阻止删除
        }
        console.log(`[Delete] Deleting property: ${String(property)}`);
        return Reflect.deleteProperty(target, property);
    }
};

let item = { id: 101, name: "Widget", description: "A useful tool" };
const securedItem = new Proxy(item, deleteGuardHandler);

console.log(delete securedItem.id);         // Output: [Delete] Cannot delete 'id' property., false
console.log(item);                          // Output: { id: 101, name: 'Widget', description: 'A useful tool' } (id unchanged)

console.log(delete securedItem.description); // Output: [Delete] Deleting property: description, true
console.log(item);                          // Output: { id: 101, name: 'Widget' }

5. apply(target, thisArg, argumentsList)

apply 陷阱用于拦截函数的调用。target 必须是一个函数。

  • 参数

    • target:被代理的函数。
    • thisArg:调用函数时传入的 this 值。
    • argumentsList:调用函数时传入的参数列表(数组)。
  • 返回值:可以是任何值,作为函数调用的结果返回。

  • 应用场景

    • 函数调用日志:记录函数的调用,包括参数和返回值。
    • 参数验证:在函数执行前验证参数的有效性。
    • 访问控制:根据权限决定是否允许执行函数。
    • 函数节流/防抖:在函数执行前后插入节流或防抖逻辑。
    • 错误处理:统一捕获函数执行中的错误。
  • 代码示例

const functionInterceptorHandler = {
    apply(target, thisArg, argumentsList) {
        console.log(`[Apply] Calling function: ${target.name || 'anonymous'} with args: ${JSON.stringify(argumentsList)}`);

        // 模拟权限检查
        const hasPermission = argumentsList.length > 0 && argumentsList[0] === 'admin_token';
        if (!hasPermission && target.name === 'deleteRecord') {
            console.error(`[Permission] Unauthorized attempt to call ${target.name}.`);
            return `Error: Unauthorized.`;
        }

        const result = Reflect.apply(target, thisArg, argumentsList);
        console.log(`[Apply] Function ${target.name || 'anonymous'} returned: ${result}`);
        return result;
    }
};

function sum(a, b) {
    return a + b;
}

function deleteRecord(id) {
    // 假设这里执行删除操作
    return `Record ${id} deleted successfully.`;
}

const interceptedSum = new Proxy(sum, functionInterceptorHandler);
const interceptedDelete = new Proxy(deleteRecord, functionInterceptorHandler);

console.log(interceptedSum(5, 3)); // Output: [Apply] Calling..., [Apply] Function... returned..., 8
console.log(interceptedDelete(123)); // Output: [Apply] Calling..., [Permission] Unauthorized..., Error: Unauthorized.
console.log(interceptedDelete('admin_token', 456)); // Output: [Apply] Calling..., [Apply] Function... returned..., Record 456 deleted successfully.

6. construct(target, argumentsList, newTarget)

construct 陷阱用于拦截 new 操作符。target 必须是一个构造函数。

  • 参数

    • target:被代理的构造函数。
    • argumentsListnew 操作符传入的参数列表。
    • newTarget:最初被调用的构造函数,通常是 Proxy 本身。
  • 返回值:必须返回一个对象。

  • 应用场景

    • 构造函数日志:记录实例的创建。
    • 参数验证:在创建实例前验证构造函数参数。
    • 自定义实例化:返回一个完全不同的对象实例,例如实现单例模式。
  • 代码示例

const constructorInterceptorHandler = {
    construct(target, argumentsList, newTarget) {
        console.log(`[Construct] Creating new instance of ${target.name || 'anonymous'} with args: ${JSON.stringify(argumentsList)}`);

        if (target.name === 'User' && argumentsList[0] === null) {
            console.error(`[Validation] User name cannot be null.`);
            throw new Error('Invalid user name.');
        }

        // 确保使用 Reflect.construct 保持正确的原型链
        const instance = Reflect.construct(target, argumentsList, newTarget);
        console.log(`[Construct] Instance created:`, instance);
        instance.createdAt = new Date(); // 为所有新实例添加创建时间
        return instance;
    }
};

class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

const InterceptedUser = new Proxy(User, constructorInterceptorHandler);

const user1 = new InterceptedUser("Alice", 30);
console.log(user1);
/* Output:
[Construct] Creating new instance of User with args: ["Alice",30]
[Construct] Instance created: User { name: 'Alice', age: 30 }
User { name: 'Alice', age: 30, createdAt: 2023-10-27T...Z }
*/

try {
    new InterceptedUser(null, 25); // Output: [Construct] Creating..., [Validation] User name cannot be null., Error: Invalid user name.
} catch (e) {
    console.error(e.message);
}

7. getOwnPropertyDescriptor(target, property)

getOwnPropertyDescriptor 陷阱用于拦截 Object.getOwnPropertyDescriptor()

  • 参数

    • target:被代理的原始对象。
    • property:要获取描述符的属性名。
  • 返回值:必须返回一个属性描述符对象(如果属性存在)或 undefined(如果属性不存在)。如果返回的描述符与真实描述符不一致,可能会导致 TypeError

  • 应用场景

    • 修改属性描述符:动态改变属性的 writable, enumerable, configurable 状态。
    • 隐藏属性:通过返回 undefined 来让属性看起来不存在。
  • 代码示例

const descriptorModifierHandler = {
    getOwnPropertyDescriptor(target, property) {
        console.log(`[Descriptor] Getting descriptor for: ${String(property)}`);
        const descriptor = Reflect.getOwnPropertyDescriptor(target, property);

        if (descriptor && property === 'secretData') {
            console.log(`[Descriptor] Modifying 'secretData' descriptor: enumerable=false`);
            descriptor.enumerable = false; // 隐藏属性,使其不可枚举
        }
        return descriptor;
    }
};

let sensitiveInfo = { id: 1, name: "Project X", secretData: "top_secret" };
const securedInfo = new Proxy(sensitiveInfo, descriptorModifierHandler);

const desc = Object.getOwnPropertyDescriptor(securedInfo, 'secretData');
console.log(desc);
/* Output:
[Descriptor] Getting descriptor for: secretData
[Descriptor] Modifying 'secretData' descriptor: enumerable=false
{ value: 'top_secret',
  writable: true,
  enumerable: false, // 已经被修改
  configurable: true }
*/

for (let key in securedInfo) {
    console.log(`[For...in] ${key}: ${securedInfo[key]}`);
}
// Output: [For...in] id: 1, [For...in] name: Project X (secretData 未被枚举)

8. defineProperty(target, property, descriptor)

defineProperty 陷阱用于拦截 Object.defineProperty()Object.defineProperties()

  • 参数

    • target:被代理的原始对象。
    • property:要定义的属性名。
    • descriptor:属性描述符对象。
  • 返回值:必须返回一个布尔值。如果属性定义成功,返回 true;如果失败,返回 false。在严格模式下,返回 false 会抛出 TypeError

  • 应用场景

    • 防止属性添加/修改:阻止新的属性被定义,或者阻止现有属性的描述符被修改。
    • 权限拦截:根据用户角色决定是否允许定义或修改属性。
    • 强制属性规则:确保所有新定义的属性都符合特定规范(例如,都是不可配置的)。
    • 日志记录:记录属性的定义操作。
  • 代码示例

const defineGuardHandler = {
    defineProperty(target, property, descriptor) {
        console.log(`[DefineProperty] Attempting to define property: ${String(property)}`);

        if (property === 'version') {
            console.warn(`[DefineProperty] Cannot define 'version' property.`);
            return false; // 阻止定义 'version' 属性
        }
        if (descriptor.configurable === false) {
            console.warn(`[DefineProperty] Forcing 'configurable' to true for ${String(property)}.`);
            descriptor.configurable = true; // 强制设置为可配置
        }

        return Reflect.defineProperty(target, property, descriptor);
    }
};

let appSettings = { theme: 'dark' };
const securedAppSettings = new Proxy(appSettings, defineGuardHandler);

Object.defineProperty(securedAppSettings, 'version', {
    value: '1.0.0',
    writable: false
});
// Output: [DefineProperty] Attempting to define property: version, [DefineProperty] Cannot define 'version' property.
console.log(appSettings); // Output: { theme: 'dark' } (version 未被添加)

Object.defineProperty(securedAppSettings, 'language', {
    value: 'en',
    writable: true,
    configurable: false // 尝试设置为不可配置
});
/* Output:
[DefineProperty] Attempting to define property: language
[DefineProperty] Forcing 'configurable' to true for language.
*/
const desc = Object.getOwnPropertyDescriptor(appSettings, 'language');
console.log(desc.configurable); // Output: true (被强制为 true)

9. getPrototypeOf(target)

getPrototypeOf 陷阱用于拦截 Object.getPrototypeOf()Reflect.getPrototypeOf()instanceof 检查以及 __proto__ 属性的读取。

  • 参数

    • target:被代理的原始对象。
  • 返回值:必须返回一个对象或 null

  • 应用场景

    • 伪造原型链:改变对象在 instanceof 检查中的行为。
    • 隐藏原型:阻止外部代码发现真实的原型链。
  • 代码示例

const prototypeModifierHandler = {
    getPrototypeOf(target) {
        console.log(`[GetPrototypeOf] Getting prototype of`, target);
        // 假设我们想让所有代理对象看起来都继承自一个特定的“虚拟”原型
        if (target.type === 'virtual') {
            return {
                isVirtual: true,
                getVirtualName() { return "VirtualObject"; }
            };
        }
        return Reflect.getPrototypeOf(target);
    }
};

let realObject = {};
let virtualObject = { type: 'virtual', data: 123 };
const realProxy = new Proxy(realObject, prototypeModifierHandler);
const virtualProxy = new Proxy(virtualObject, prototypeModifierHandler);

console.log(Object.getPrototypeOf(realProxy));   // Output: [GetPrototypeOf] Getting prototype of {}, {}
console.log(Object.getPrototypeOf(virtualProxy)); // Output: [GetPrototypeOf] Getting prototype of { type: 'virtual', data: 123 }, { isVirtual: true, getVirtualName: [Function: getVirtualName] }

10. setPrototypeOf(target, prototype)

setPrototypeOf 陷阱用于拦截 Object.setPrototypeOf()

  • 参数

    • target:被代理的原始对象。
    • prototype:要设置的新原型对象(或 null)。
  • 返回值:必须返回一个布尔值。如果原型设置成功,返回 true;如果失败,返回 false

  • 应用场景

    • 防止原型链修改:阻止外部代码改变对象的原型。
    • 日志记录:记录原型链的变更尝试。
  • 代码示例

const preventSetPrototypeHandler = {
    setPrototypeOf(target, prototype) {
        console.log(`[SetPrototypeOf] Attempting to set prototype of`, target, `to`, prototype);
        if (target.isLocked) {
            console.warn(`[SetPrototypeOf] Object is locked, prototype cannot be changed.`);
            return false; // 阻止修改原型
        }
        return Reflect.setPrototypeOf(target, prototype);
    }
};

let mutableObject = { a: 1 };
let lockedObject = { b: 2, isLocked: true };

const mutableProxy = new Proxy(mutableObject, preventSetPrototypeHandler);
const lockedProxy = new Proxy(lockedObject, preventSetPrototypeHandler);

const newProto = { c: 3 };

console.log(Object.setPrototypeOf(mutableProxy, newProto)); // Output: [SetPrototypeOf] Attempting..., true
console.log(Object.getPrototypeOf(mutableProxy));          // Output: { c: 3 }

console.log(Object.setPrototypeOf(lockedProxy, newProto));  // Output: [SetPrototypeOf] Attempting..., [SetPrototypeOf] Object is locked..., false
console.log(Object.getPrototypeOf(lockedProxy));           // Output: {} (原型未变)

11. enumerate(target) (已废弃,推荐使用 ownKeys)

enumerate 陷阱在 ES6 规范中曾用于拦截 for...in 循环,但它已被 ownKeys 陷阱取代,并且不再是标准的一部分。虽然一些旧的 JavaScript 环境可能仍然支持它,但在现代代码中应避免使用。为了完整性,这里简要提及。

  • 参数

    • target:被代理的原始对象。
  • 返回值:必须返回一个迭代器对象。

  • 代码示例 (仅作示意,不推荐在生产环境使用)

// const enumerateHandler = {
//     enumerate(target) {
//         console.log(`[Enumerate] Intercepting for...in enumeration.`);
//         return Reflect.enumerate(target); // 通常直接转发
//     }
// };
//
// let myObject = { a: 1, b: 2 };
// const myProxy = new Proxy(myObject, enumerateHandler);
//
// for (let key in myProxy) {
//     console.log(key);
// }

12. ownKeys(target)

ownKeys 陷阱用于拦截 Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()for...in 循环以及 JSON.stringify()。它是 enumerate 的现代替代品,功能更强大。

  • 参数

    • target:被代理的原始对象。
  • 返回值:必须返回一个由字符串或 Symbol 组成的数组。

  • 应用场景

    • 隐藏私有属性:让某些属性不出现在属性列表中。
    • 动态属性列表:根据条件改变可枚举属性的列表。
    • 权限拦截:控制用户能看到哪些属性。
  • 代码示例

const ownKeysHandler = {
    ownKeys(target) {
        console.log(`[OwnKeys] Intercepting ownKeys operation for`, target);
        const keys = Reflect.ownKeys(target);
        // 过滤掉以 '_' 开头的私有属性
        const filteredKeys = keys.filter(key => !String(key).startsWith('_'));
        console.log(`[OwnKeys] Returning filtered keys: ${JSON.stringify(filteredKeys)}`);
        return filteredKeys;
    }
};

let dataObject = { publicA: 1, publicB: 2, _privateC: 3, [Symbol('privateD')]: 4 };
const securedDataObject = new Proxy(dataObject, ownKeysHandler);

console.log(Object.keys(securedDataObject));          // Output: [OwnKeys]..., [OwnKeys]..., ["publicA", "publicB"]
console.log(Object.getOwnPropertyNames(securedDataObject)); // Output: [OwnKeys]..., [OwnKeys]..., ["publicA", "publicB"]
console.log(Object.getOwnPropertySymbols(securedDataObject)); // Output: [OwnKeys]..., [OwnKeys]..., [] (Symbol 也被过滤了,因为startsWith('_')对Symbol转字符串后不符合)

// for...in 也受 ownKeys 影响
for (let key in securedDataObject) {
    console.log(`[For...in] ${key}: ${securedDataObject[key]}`);
}
// Output: [For...in] publicA: 1, [For...in] publicB: 2

13. isExtensible(target)

isExtensible 陷阱用于拦截 Object.isExtensible()

  • 参数

    • target:被代理的原始对象。
  • 返回值:必须返回一个布尔值。

  • 应用场景

    • 伪造可扩展性:让对象看起来不可扩展,即使它实际上可以添加新属性。
  • 代码示例

const extensibleHandler = {
    isExtensible(target) {
        console.log(`[IsExtensible] Checking extensibility of`, target);
        // 假设我们想让所有对象看起来都不可扩展
        return false;
    }
};

let myPlainObject = {};
const nonExtensibleProxy = new Proxy(myPlainObject, extensibleHandler);

console.log(Object.isExtensible(myPlainObject));     // Output: true (原始对象可扩展)
console.log(Object.isExtensible(nonExtensibleProxy)); // Output: [IsExtensible]..., false (代理对象看起来不可扩展)

// 尝试向代理添加新属性,会因为代理说不可扩展而在严格模式下报错
try {
    nonExtensibleProxy.newProp = "test";
} catch (e) {
    console.error(e.message); // Output: TypeError: 'set' on proxy: property 'newProp' cannot be added to non-extensible object
}

14. preventExtensions(target)

preventExtensions 陷阱用于拦截 Object.preventExtensions()

  • 参数

    • target:被代理的原始对象。
  • 返回值:必须返回一个布尔值。如果操作成功,返回 true;如果失败,返回 false

  • 应用场景

    • 强制不可扩展:在调用 Object.preventExtensions() 时执行额外逻辑。
    • 日志记录:记录对象被设置为不可扩展的操作。
  • 代码示例

const preventExtensionLoggerHandler = {
    preventExtensions(target) {
        console.log(`[PreventExtensions] Attempting to prevent extensions on`, target);
        // 可以添加额外的检查或清理逻辑
        if (target.canBeExtended === false) {
             console.warn(`[PreventExtensions] Target already cannot be extended.`);
             return false; // 假设 target 自身有一些不可扩展的标志
        }
        const result = Reflect.preventExtensions(target);
        if (result) {
            console.log(`[PreventExtensions] Successfully prevented extensions.`);
        } else {
            console.error(`[PreventExtensions] Failed to prevent extensions.`);
        }
        return result;
    }
};

let dataContainer = { value: 100, canBeExtended: true };
const preventExtensibleProxy = new Proxy(dataContainer, preventExtensionLoggerHandler);

console.log(Object.isExtensible(preventExtensibleProxy)); // Output: true (默认可扩展)
Object.preventExtensions(preventExtensibleProxy);         // Output: [PreventExtensions]..., [PreventExtensions]...Successfully...
console.log(Object.isExtensible(preventExtensibleProxy)); // Output: false (现在不可扩展)

let rigidContainer = { value: 200, canBeExtended: false };
const rigidProxy = new Proxy(rigidContainer, preventExtensionLoggerHandler);
console.log(Object.preventExtensions(rigidProxy)); // Output: [PreventExtensions]..., [PreventExtensions]...Target already cannot be extended., false

结合陷阱实现响应式与权限拦截

理解了单个陷阱的功能后,我们可以将它们组合起来,构建更复杂的系统。

响应式数据系统(类 Vue 3)

一个简化的响应式系统需要 get 来追踪依赖,set 来触发更新,以及 deleteProperty 来处理属性删除。

核心思路:

  1. 依赖收集 (Dependency Collection):当一个响应式属性被 get 访问时,记录下当前正在运行的“副作用”(effect,例如一个组件的渲染函数)与该属性的关联。
  2. 派发更新 (Trigger Update):当一个响应式属性被 set 修改时,找到所有依赖于该属性的副作用,并执行它们。
  3. 处理删除:当属性被删除时,也需要触发相关依赖的更新。
// 模拟一个全局的活跃副作用函数栈
const effectStack = [];
// WeakMap: target -> Map<key, Set<effects>>
const targetMap = new WeakMap();

// 依赖收集
function track(target, key) {
    const effect = effectStack[effectStack.length - 1];
    if (effect) {
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            depsMap = new Map();
            targetMap.set(target, depsMap);
        }
        let dep = depsMap.get(key);
        if (!dep) {
            dep = new Set();
            depsMap.set(key, dep);
        }
        dep.add(effect);
        console.log(`[Reactive] Tracking effect for ${String(key)}.`);
    }
}

// 派发更新
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;

    const dep = depsMap.get(key);
    if (dep) {
        console.log(`[Reactive] Triggering effects for ${String(key)}.`);
        dep.forEach(effect => effect());
    }
}

// 创建响应式对象
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key, receiver) {
            track(target, key); // 依赖收集
            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            const oldValue = Reflect.get(target, key, receiver);
            const result = Reflect.set(target, key, value, receiver);
            if (oldValue !== value) { // 值发生变化才触发
                trigger(target, key); // 派发更新
            }
            return result;
        },
        deleteProperty(target, key) {
            const hasKey = Reflect.has(target, key);
            const result = Reflect.deleteProperty(target, key);
            if (hasKey && result) { // 只有成功删除了已存在的属性才触发
                trigger(target, key); // 派发更新
            }
            return result;
        },
        // 确保for...in, Object.keys等也能触发依赖
        has(target, key) {
            track(target, key);
            return Reflect.has(target, key);
        },
        ownKeys(target) {
            // 对于数组,这里可以追踪length属性
            track(target, 'length'); // 简化处理,所有迭代都追踪length
            return Reflect.ownKeys(target);
        }
    });
}

// 模拟一个副作用函数
function effect(fn) {
    effectStack.push(fn); // 将当前副作用推入栈中
    fn(); // 立即执行一次,进行依赖收集
    effectStack.pop(); // 执行完毕后弹出
}

// 示例
const state = reactive({ count: 0, message: "Hello", list: [1, 2] });

console.log("--- Initial effects ---");
effect(() => {
    console.log(`Effect 1: Count is ${state.count}`);
});
effect(() => {
    console.log(`Effect 2: Message is "${state.message}"`);
});
effect(() => {
    console.log(`Effect 3: List length is ${state.list.length}`);
});

console.log("n--- Modifying count ---");
state.count++; // Output: [Reactive] Setting..., [Reactive] Triggering..., Effect 1: Count is 1

console.log("n--- Modifying message ---");
state.message = "World"; // Output: [Reactive] Setting..., [Reactive] Triggering..., Effect 2: Message is "World"

console.log("n--- Adding to list ---");
state.list.push(3); // Output: [Reactive] Setting..., [Reactive] Triggering..., Effect 3: List length is 3 (由于数组操作会改变length)

console.log("n--- Deleting property ---");
delete state.message; // Output: [Reactive] Deleting..., [Reactive] Triggering..., Effect 2: Message is "undefined"

console.log("n--- Accessing non-existent property ---");
console.log('nonExistent' in state); // Output: [Reactive] Tracking..., false

细粒度权限拦截

通过组合 get, set, deleteProperty, apply, construct 等,我们可以实现非常精细的权限控制。

const roles = {
    ADMIN: ['read', 'write', 'delete', 'execute'],
    EDITOR: ['read', 'write'],
    VIEWER: ['read']
};

function createSecureProxy(target, userRole) {
    const userPermissions = roles[userRole] || [];

    const checkPermission = (operation, property = null) => {
        if (!userPermissions.includes(operation)) {
            const msg = property ?
                `User role '${userRole}' is not allowed to ${operation} property '${String(property)}'.` :
                `User role '${userRole}' is not allowed to ${operation} this resource.`;
            console.warn(`[Security] ${msg}`);
            return false;
        }
        return true;
    };

    return new Proxy(target, {
        get(target, property, receiver) {
            if (!checkPermission('read', property)) {
                return undefined; // 或者抛出错误
            }
            return Reflect.get(target, property, receiver);
        },
        set(target, property, value, receiver) {
            if (!checkPermission('write', property)) {
                return false;
            }
            return Reflect.set(target, property, value, receiver);
        },
        deleteProperty(target, property) {
            if (!checkPermission('delete', property)) {
                return false;
            }
            return Reflect.deleteProperty(target, property);
        },
        apply(target, thisArg, argumentsList) {
            if (typeof target === 'function' && !checkPermission('execute')) {
                return () => console.warn(`[Security] User role '${userRole}' is not allowed to execute this function.`);
            }
            return Reflect.apply(target, thisArg, argumentsList);
        },
        construct(target, argumentsList, newTarget) {
            if (typeof target === 'function' && !checkPermission('execute')) {
                throw new Error(`[Security] User role '${userRole}' is not allowed to construct new instances.`);
            }
            return Reflect.construct(target, argumentsList, newTarget);
        },
        // 隐藏不具备读权限的属性
        has(target, property) {
            if (!checkPermission('read', property)) {
                return false;
            }
            return Reflect.has(target, property);
        },
        ownKeys(target) {
            const allKeys = Reflect.ownKeys(target);
            return allKeys.filter(key => checkPermission('read', key));
        }
    });
}

// 示例数据和函数
const projectData = {
    id: 'P101',
    name: 'Secret Project',
    budget: 1000000,
    status: 'In Progress',
    _internalNotes: 'Highly confidential details'
};

class TaskManager {
    constructor(project) { this.project = project; }
    addTask(taskName) { console.log(`Adding task "${taskName}" to ${this.project.name}`); return true; }
    deleteTask(taskId) { console.log(`Deleting task ${taskId} from ${this.project.name}`); return true; }
}

const taskManagerInstance = new TaskManager(projectData);

// 创建不同角色的代理
const adminProject = createSecureProxy(projectData, 'ADMIN');
const editorProject = createSecureProxy(projectData, 'EDITOR');
const viewerProject = createSecureProxy(projectData, 'VIEWER');

const adminTaskManager = createSecureProxy(taskManagerInstance.addTask.bind(taskManagerInstance), 'ADMIN');
const editorTaskManager = createSecureProxy(taskManagerInstance.addTask.bind(taskManagerInstance), 'EDITOR');
const viewerTaskManager = createSecureProxy(taskManagerInstance.addTask.bind(taskManagerInstance), 'VIEWER');

console.log("--- Admin Access ---");
console.log(adminProject.name);      // Output: Secret Project
adminProject.budget = 1200000;       // Output: (no warn)
console.log(adminProject.budget);    // Output: 1200000
delete adminProject._internalNotes;  // Output: (no warn)
console.log(adminTaskManager("Admin task")); // Output: Adding task "Admin task"...

console.log("n--- Editor Access ---");
console.log(editorProject.name);     // Output: Secret Project
editorProject.status = 'Completed';  // Output: (no warn)
console.log(editorProject.status);   // Output: Completed
editorProject.budget = 1500000;      // Output: [Security] User role 'EDITOR' is not allowed to write property 'budget'.
console.log(editorProject.budget);   // Output: 1200000 (unchanged)
delete editorProject._internalNotes; // Output: [Security] User role 'EDITOR' is not allowed to delete property '_internalNotes'.
console.log(editorTaskManager("Editor task")); // Output: Adding task "Editor task"...

console.log("n--- Viewer Access ---");
console.log(viewerProject.name);     // Output: Secret Project
console.log(viewerProject.budget);   // Output: 1200000
viewerProject.status = 'Archived';   // Output: [Security] User role 'VIEWER' is not allowed to write property 'status'.
console.log(viewerProject.status);   // Output: Completed (unchanged)
console.log(viewerProject._internalNotes); // Output: [Security] User role 'VIEWER' is not allowed to read property '_internalNotes'., undefined
console.log('budget' in viewerProject); // Output: true
console.log('_internalNotes' in viewerProject); // Output: [Security] User role 'VIEWER' is not allowed to read property '_internalNotes'., false
console.log(Object.keys(viewerProject)); // Output: [Security] User role 'VIEWER' is not allowed to read property '_internalNotes'., [ 'id', 'name', 'budget', 'status' ]
viewerTaskManager("Viewer task"); // Output: [Security] User role 'VIEWER' is not allowed to execute this function.

性能考量与最佳实践

Proxy 带来了极大的灵活性,但并非没有代价。每次对代理对象的操作都会经过陷阱函数的处理,这会引入一定的性能开销。

  • 性能开销:与直接操作原始对象相比,Proxy 肯定会慢一些。对于性能要求极高的场景,需要仔细权衡。然而,对于大多数应用程序而言,Proxy 带来的抽象和功能优势通常远大于其微小的性能损失。
  • Reflect 的重要性:始终使用 Reflect API 在陷阱中执行默认操作。这不仅能保证行为的正确性,还能提高代码的健壮性和可读性。直接操作 target 可能会在某些情况下导致 this 上下文问题或意外行为。
  • Revocable Proxies (可撤销代理)Proxy.revocable(target, handler) 可以创建一个可撤销的代理。一旦代理被撤销,所有对其的后续操作都会抛出 TypeError。这在处理敏感对象或需要临时授权的场景中非常有用。
  • Proxy 链:可以创建 Proxy 的 Proxy,形成一个拦截链。这允许你分层地添加不同的拦截逻辑。
  • 避免无限递归:在 getset 陷阱中,如果你在陷阱内部再次访问代理对象,需要确保不会形成无限递归。例如,在 get 陷阱中直接 return this[property] 而不是 Reflect.get(target, property, receiver) 可能会导致递归。

总结

ES6 Proxy 是 JavaScript 语言中一项革命性的元编程特性,它通过 13 种拦截陷阱,提供了对对象底层操作的强大控制能力。无论是构建像 Vue 3 那样的响应式数据系统,实现细致入微的权限管理,还是进行数据验证、日志记录等,Proxy 都提供了一个优雅且高效的解决方案。掌握这些陷阱,将极大地拓宽你在 JavaScript 应用开发中的设计思路和实现能力。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注