JS `Proxy` 对象:拦截并自定义对象的基本操作

各位观众老爷们,早上好/下午好/晚上好! 今天咱们聊点有意思的,关于 JavaScript 里那个神秘又强大的 Proxy 对象。 保证让你们听完之后,感觉自己也能像个魔术师一样,操控对象的行为了。

开场白:什么是 Proxy?

想象一下,你有个好朋友,叫 originalObject。 你想送它一些东西,但是你不想直接把东西给它,而是想让一个中间人 proxyObject 先处理一下,比如检查一下东西是不是符合朋友的口味,或者加个包装啥的。 这个 proxyObject 就是我们今天的主角,Proxy

简单来说,Proxy 对象允许你创建一个对象的代理,你可以拦截并自定义对该对象的基本操作(例如属性查找、赋值、枚举、函数调用等)。 就像一个看门老大爷,守着你家的宝贝,谁想动一下,都得先经过他的同意。

Proxy 的基本语法

Proxy 对象的语法很简单:

const proxy = new Proxy(target, handler);
  • target: 你想代理的目标对象。 可以是普通对象、数组、函数,甚至另一个 Proxy
  • handler: 一个对象,定义了各种“陷阱”(traps),也就是你想要拦截的操作。 这些陷阱都是函数,它们会在对应的操作发生时被调用。

Handler 对象:陷阱大集合

handler 对象是 Proxy 的灵魂所在。 它定义了各种陷阱,让你可以定制对目标对象的操作。 我们来看看一些常用的陷阱:

陷阱 (Trap) 触发条件
get 读取对象的属性时触发。 比如 obj.propertyobj['property']
set 设置对象的属性时触发。 比如 obj.property = valueobj['property'] = value
has 使用 in 操作符检查对象是否具有某个属性时触发。 比如 'property' in obj
deleteProperty 使用 delete 操作符删除对象的属性时触发。 比如 delete obj.property
ownKeys 使用 Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys() 方法时触发。
apply 当目标对象是函数,并且被调用时触发。 比如 proxy(arguments)
construct 当目标对象是构造函数,并且使用 new 操作符调用时触发。 比如 new proxy(arguments)

实战演练:拦截属性读取 (get)

咱们先来个简单的例子,拦截属性读取操作:

const person = {
    name: '张三',
    age: 30
};

const handler = {
    get: function(target, property, receiver) {
        console.log(`有人想知道我的 ${property}!`);
        if (property === 'age') {
            return '年龄保密!'; // 年龄是秘密!
        }
        return Reflect.get(target, property, receiver); // 调用默认行为
    }
};

const proxyPerson = new Proxy(person, handler);

console.log(proxyPerson.name); // 输出: 有人想知道我的 name!n张三
console.log(proxyPerson.age);  // 输出: 有人想知道我的 age!n年龄保密!

在这个例子中,我们创建了一个 person 对象,然后用 Proxy 代理它。 handler 对象中的 get 陷阱会在读取 proxyPerson 的属性时被调用。 当有人想知道 age 时,我们直接返回 "年龄保密!",否则调用 Reflect.get() 来执行默认的属性读取行为。

Reflect 对象:Proxy 的好帮手

你可能注意到了,在 get 陷阱里,我们用到了 Reflect.get()Reflect 对象是一个内建对象,它提供了一系列与 Proxy handler 方法对应的静态方法。 它的目的是提供一个默认的操作行为,方便我们在陷阱中进行自定义处理后,再决定是否执行默认行为。

使用 Reflect 的好处:

  • 更清晰的代码: Reflect 方法的参数顺序和 Proxy handler 方法的参数顺序一致,更易于理解。
  • 避免错误: 直接使用 target[property] 可能会导致一些意想不到的错误,而 Reflect.get() 会更安全地执行属性读取操作。
  • 继承: Reflect 可以正确处理继承关系,保证 this 指向正确的对象。

实战演练:拦截属性设置 (set)

接下来,我们看看如何拦截属性设置操作:

const person = {
    name: '张三',
    age: 30
};

const handler = {
    set: function(target, property, value, receiver) {
        console.log(`有人想把我的 ${property} 改成 ${value}!`);
        if (property === 'age' && typeof value !== 'number') {
            console.log('年龄必须是数字!');
            return false; // 阻止设置
        }
        return Reflect.set(target, property, value, receiver); // 调用默认行为
    }
};

const proxyPerson = new Proxy(person, handler);

proxyPerson.name = '李四'; // 输出: 有人想把我的 name 改成 李四!
console.log(proxyPerson.name); // 输出: 李四

proxyPerson.age = '二十八'; // 输出: 有人想把我的 age 改成 二十八!n年龄必须是数字!
console.log(proxyPerson.age); // 输出: 30 (未被修改)

proxyPerson.age = 28; // 输出: 有人想把我的 age 改成 28!
console.log(proxyPerson.age); // 输出: 28

在这个例子中,set 陷阱会在设置 proxyPerson 的属性时被调用。 我们检查了 age 的类型,如果不是数字,就阻止设置,并输出错误信息。

实战演练:数据验证

Proxy 非常适合用于数据验证。 我们可以定义一个 validator 函数,在 set 陷阱中检查数据的有效性:

function createValidator(target, validator) {
    return new Proxy(target, {
        set: function(target, property, value, receiver) {
            if (validator[property] && !validator[property](value)) {
                console.log(`Invalid value for ${property}: ${value}`);
                return false; // 阻止设置
            }
            return Reflect.set(target, property, value, receiver);
        }
    });
}

const person = {
    name: '张三',
    age: 30,
    email: '[email protected]'
};

const validator = {
    age: function(age) {
        return typeof age === 'number' && age >= 0 && age < 150;
    },
    email: function(email) {
        return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
    }
};

const proxyPerson = createValidator(person, validator);

proxyPerson.age = -10; // 输出: Invalid value for age: -10
proxyPerson.email = 'invalid-email'; // 输出: Invalid value for email: invalid-email

console.log(proxyPerson); // 输出: { name: '张三', age: 30, email: '[email protected]' } (未被修改)

proxyPerson.age = 25;
proxyPerson.email = '[email protected]';

console.log(proxyPerson); // 输出: { name: '张三', age: 25, email: '[email protected]' }

实战演练:隐藏属性

有时候,我们希望对象的一些属性是私有的,不希望外部直接访问。 Proxy 可以帮助我们实现这个目标:

const secretData = {
    _id: '123456', // 内部 ID,不希望外部访问
    username: 'john_doe',
    email: '[email protected]'
};

const handler = {
    get: function(target, property, receiver) {
        if (property.startsWith('_')) {
            console.log('Access denied!');
            return undefined; // 阻止访问
        }
        return Reflect.get(target, property, receiver);
    },
    set: function(target, property, value, receiver) {
        if (property.startsWith('_')) {
            console.log('Modification denied!');
            return false; // 阻止修改
        }
        return Reflect.set(target, property, value, receiver);
    },
    has: function(target, property) {
        if (property.startsWith('_')) {
            return false; // 隐藏属性
        }
        return Reflect.has(target, property);
    },
    ownKeys: function(target) {
        return Reflect.ownKeys(target).filter(key => !key.startsWith('_')); // 隐藏属性
    }
};

const proxyData = new Proxy(secretData, handler);

console.log(proxyData.username); // 输出: john_doe
console.log(proxyData._id);      // 输出: Access denied!nundefined

proxyData.username = 'jane_doe';
console.log(proxyData.username); // 输出: jane_doe

proxyData._id = '789012';      // 输出: Modification denied!
console.log(proxyData._id);      // 输出: undefined

console.log('_id' in proxyData); // 输出: false
console.log('username' in proxyData); // 输出: true

console.log(Object.keys(proxyData)); // 输出: [ 'username', 'email' ]

在这个例子中,我们约定以下划线 _ 开头的属性是私有的。 Proxy 拦截了 getsethasownKeys 操作,隐藏了这些私有属性。

实战演练:函数代理 (apply, construct)

Proxy 还可以代理函数。 apply 陷阱会在函数被调用时触发,construct 陷阱会在函数作为构造函数被调用时触发。

const greet = function(name) {
    return `Hello, ${name}!`;
};

const handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log('有人想跟我打招呼!');
        const name = argumentsList[0] || '陌生人';
        return target.apply(thisArg, [name.toUpperCase()]); // 统一转换成大写
    },
    construct: function(target, argumentsList, newTarget) {
        console.log('有人想用我创建一个新的对象!');
        const obj = new target(...argumentsList);
        obj.createdAt = new Date();
        return obj;
    }
};

const proxyGreet = new Proxy(greet, handler);

console.log(proxyGreet('world')); // 输出: 有人想跟我打招呼!nHello, WORLD!
console.log(proxyGreet());       // 输出: 有人想跟我打招呼!nHello, 陌生人!

function Person(name) {
    this.name = name;
}

const proxyPerson = new Proxy(Person, handler);
const person = new proxyPerson('Alice'); // 输出: 有人想用我创建一个新的对象!
console.log(person); // 输出: Person { name: 'Alice', createdAt: 2023-10-27T... }

Proxy 的一些注意事项

  • 性能: Proxy 会增加一些性能开销,因为它需要在每次操作时都进行拦截和处理。 因此,在性能敏感的场景下,需要谨慎使用。
  • 不可枚举属性: Proxy 默认不会暴露目标对象的不可枚举属性。 如果需要暴露这些属性,需要在 ownKeys 陷阱中进行处理。
  • 循环引用: 如果 Proxy 代理了自身,可能会导致循环引用,造成栈溢出。 需要避免这种情况。
  • 调试: 调试 Proxy 可能会比较困难,因为代码的执行流程会变得更加复杂。 可以使用调试工具来跟踪 Proxy 的行为。
  • 并非万能: 有一些操作 Proxy 无法拦截,例如 instanceof 操作符,因为它们直接操作的是对象的原型链。

Proxy 的应用场景

  • 数据验证: 在数据赋值时进行验证,确保数据的有效性。
  • 访问控制: 限制对对象属性的访问,实现私有属性或权限控制。
  • 日志记录: 记录对对象的操作,方便调试和分析。
  • 虚拟化: 创建虚拟对象,延迟加载数据或模拟复杂的数据结构。
  • 撤销代理: 使用 Proxy.revocable() 可以创建一个可以被撤销的代理对象,一旦撤销,代理对象将无法使用。

总结

Proxy 对象是一个强大的工具,可以让你拦截并自定义对对象的基本操作。 它可以用于数据验证、访问控制、日志记录等多种场景。 但是,Proxy 也会增加一些性能开销,需要谨慎使用。

希望今天的讲座能让你对 Proxy 对象有更深入的了解。 现在,你可以像个魔术师一样,操控你的 JavaScript 对象了!

各位,下课!

发表回复

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