各位观众老爷们,早上好/下午好/晚上好! 今天咱们聊点有意思的,关于 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.property 或 obj['property'] 。 |
set |
设置对象的属性时触发。 比如 obj.property = value 或 obj['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
拦截了 get
、set
、has
和 ownKeys
操作,隐藏了这些私有属性。
实战演练:函数代理 (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 对象了!
各位,下课!