各位靓仔靓女,晚上好!我是今晚的讲师,大家可以叫我老王。今天咱们聊聊JavaScript里一个挺有意思的家伙——Proxy
,以及它在ORM和数据拦截中的骚操作。别紧张,听老王白话白话,保证你听得懂,用得上,还能在同事面前装一波。
一、Proxy是啥玩意儿?别整那些官方术语,说人话!
Proxy
,翻译过来就是“代理”。 就像明星有经纪人,你的代码也需要一个“中间人”来帮你处理一些事情。 这个中间人,就是Proxy
。
简单来说,Proxy
允许你拦截并自定义对象的基本操作,比如读取属性、设置属性、调用函数等等。你可以理解为给对象套上了一层“外壳”,所有对该对象的操作都要先经过这层外壳的“盘问”,你可以在这个过程中做一些手脚。
二、Proxy的基本语法:这玩意儿怎么用?
const target = { // 目标对象,也就是你想代理的对象
name: '张三',
age: 30
};
const handler = { // 处理器对象,定义了拦截的行为
get: function(target, property, receiver) {
console.log(`有人想读取我的${property}属性!`);
return Reflect.get(target, property, receiver); // 必须返回,否则读不到值
},
set: function(target, property, value, receiver) {
console.log(`有人想修改我的${property}属性为${value}!`);
Reflect.set(target, property, value, receiver); // 必须设置,否则改不了值
return true; // 表示设置成功
}
};
const proxy = new Proxy(target, handler); // 创建代理对象
console.log(proxy.name); // 输出:有人想读取我的name属性! 张三
proxy.age = 35; // 输出:有人想修改我的age属性为35!
console.log(target.age); // 输出:35 (target的值也被修改了)
代码解释:
target
: 这是你想要代理的原始对象。 就像你想让你的房子出租,target
就是你的房子。handler
: 这是一个对象,包含了各种“陷阱”(traps)。 陷阱就是拦截特定操作的函数。 比如get
拦截属性读取,set
拦截属性设置。 就像是你的房屋中介,负责处理租客的各种要求。proxy
: 这是代理对象。 以后你操作proxy
,实际上是通过handler
来操作target
。 就像租客找房屋中介租房,实际住的是你的房子。Reflect
: 这是一个内置对象,提供了一些方法,可以调用与Proxy
陷阱相同的方法。Reflect.get
相当于target[property]
,Reflect.set
相当于target[property] = value
。 重点: 在handler
里,你最好使用Reflect
来操作target
,这样可以避免一些奇怪的问题,并保持行为的一致性。 必须返回Reflect.get
或者Reflect.set
的结果,否则会出问题!
常用的Proxy陷阱(Traps):
陷阱 (Trap) | 拦截的操作 | 作用 |
---|---|---|
get(target, property, receiver) |
读取属性 | 拦截读取属性的操作,可以自定义返回值,或者抛出错误。 |
set(target, property, value, receiver) |
设置属性 | 拦截设置属性的操作,可以进行验证,或者阻止设置。 |
apply(target, thisArg, argumentsList) |
调用函数 | 拦截函数调用,可以修改参数,或者修改返回值。 |
construct(target, argumentsList, newTarget) |
new 操作符 |
拦截 new 操作符,可以修改 new 的行为。 |
defineProperty(target, property, descriptor) |
Object.defineProperty |
拦截 Object.defineProperty 操作,可以自定义属性的定义。 |
deleteProperty(target, property) |
delete 操作符 |
拦截 delete 操作符,可以阻止删除属性。 |
has(target, property) |
in 操作符 |
拦截 in 操作符,可以自定义 in 的行为。 |
ownKeys(target) |
Object.getOwnPropertyNames 和 Object.getOwnPropertySymbols |
拦截获取对象自身属性的操作,可以过滤属性。 |
getOwnPropertyDescriptor(target, property) |
Object.getOwnPropertyDescriptor |
拦截获取属性描述符的操作,可以自定义属性描述符。 |
getPrototypeOf(target) |
Object.getPrototypeOf |
拦截获取原型对象的操作,可以自定义原型对象。 |
setPrototypeOf(target, prototype) |
Object.setPrototypeOf |
拦截设置原型对象的操作,可以阻止设置原型对象。 |
preventExtensions(target) |
Object.preventExtensions |
拦截阻止对象扩展的操作,可以阻止阻止对象扩展。 |
isExtensible(target) |
Object.isExtensible |
拦截判断对象是否可扩展的操作,可以自定义判断结果。 |
三、Proxy在ORM中的应用:让数据库操作更优雅
ORM(Object-Relational Mapping),即对象关系映射。 简单来说,就是把数据库中的表映射成对象,让你用面向对象的方式操作数据库。
Proxy
可以用来增强 ORM 的功能,比如:
- 延迟加载 (Lazy Loading): 只有在真正需要的时候才从数据库加载数据。
// 假设我们有一个 User 类,对应数据库中的 user 表
class User {
constructor(id) {
this.id = id;
}
}
// 模拟数据库查询
function getUserFromDB(id) {
console.log(`从数据库查询 User id=${id}`);
return { id, name: '李四', age: 25 }; // 模拟数据库返回的数据
}
const userProxyHandler = {
get: function(target, property, receiver) {
if (target[property] === undefined) { // 如果属性未加载
const userData = getUserFromDB(target.id); // 从数据库加载数据
Object.assign(target, userData); // 将数据赋值给 target
console.log(`延迟加载了 ${property} 属性`);
}
return Reflect.get(target, property, receiver);
}
};
const user = new User(123); // 创建 User 对象,但数据还没加载
const userProxy = new Proxy(user, userProxyHandler);
console.log(userProxy.name); // 输出:从数据库查询 User id=123 延迟加载了 name 属性 李四
console.log(userProxy.age); // 输出:延迟加载了 age 属性 25
console.log(userProxy.id); // 输出:123 (id 已经在构造函数中加载了)
代码解释:
- 一开始,
User
对象只有id
属性。 - 当我们访问
userProxy.name
时,由于name
属性未定义,get
陷阱会被触发。 get
陷阱会从数据库加载User
的数据,并赋值给user
对象。- 下次访问
userProxy.name
或userProxy.age
时,就不会再从数据库加载了,因为数据已经存在了。
- 数据验证 (Data Validation): 在数据写入数据库之前进行验证。
const product = {
name: '',
price: 0
};
const productProxyHandler = {
set: function(target, property, value, receiver) {
if (property === 'price') {
if (typeof value !== 'number' || value <= 0) {
throw new Error('价格必须是大于0的数字');
}
}
Reflect.set(target, property, value, receiver);
return true;
}
};
const productProxy = new Proxy(product, productProxyHandler);
productProxy.name = 'iPhone 15';
productProxy.price = 9999; // 正常设置
try {
productProxy.price = -10; // 抛出错误:价格必须是大于0的数字
} catch (error) {
console.error(error.message);
}
代码解释:
set
陷阱拦截了属性设置操作。- 当设置
price
属性时,set
陷阱会检查value
是否为大于 0 的数字。 - 如果不是,则抛出错误,阻止设置。
- 字段转换 (Field Transformation): 自动将数据库字段名转换为 JavaScript 风格的命名(例如,将
user_name
转换为userName
)。
const databaseRecord = {
user_name: '王五',
user_age: 28
};
function camelCase(str) { // 辅助函数,将下划线命名转换为驼峰命名
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
}
const recordProxyHandler = {
get: function(target, property, receiver) {
const dbFieldName = property.replace(/([A-Z])/g, '_$1').toLowerCase(); // 将驼峰命名转换为下划线命名,尝试从数据库查找
if (target[property] === undefined) {
if (target[dbFieldName] !== undefined) {
target[property] = target[dbFieldName];
}
}
return Reflect.get(target, property, receiver);
}
};
const recordProxy = new Proxy(databaseRecord, recordProxyHandler);
console.log(recordProxy.userName); // 输出:王五 (自动将 user_name 转换为 userName)
console.log(recordProxy.userAge); // 输出:28 (自动将 user_age 转换为 userAge)
代码解释:
get
陷阱拦截了属性读取操作。- 在
get
陷阱中,我们将属性名从驼峰命名转换为下划线命名,然后在target
对象中查找对应的属性。 - 如果找到了,就将值返回。
四、Proxy在数据拦截中的应用:掌控数据的流动
Proxy
在数据拦截方面也有很多用途,比如:
- 数据劫持 (Data Binding): 实现响应式数据,当数据发生变化时,自动更新 UI。 Vue 2.x 就是用
Object.defineProperty
实现的,而 Vue 3.x 已经改用Proxy
了,性能更好。
// 模拟一个简单的响应式系统
function observe(obj, onChange) {
return new Proxy(obj, {
set: function(target, property, value, receiver) {
const oldValue = target[property];
Reflect.set(target, property, value, receiver);
if (oldValue !== value) {
onChange(property, oldValue, value); // 数据变化时,触发回调
}
return true;
}
});
}
const data = {
message: 'Hello'
};
const reactiveData = observe(data, (property, oldValue, newValue) => {
console.log(`属性 ${property} 从 ${oldValue} 变成了 ${newValue}`);
// 在这里更新 UI
});
reactiveData.message = 'World'; // 输出:属性 message 从 Hello 变成了 World
代码解释:
observe
函数接收一个对象和一个回调函数。- 它使用
Proxy
拦截set
操作。 - 当数据发生变化时,
set
陷阱会调用回调函数,通知 UI 更新。
- 访问控制 (Access Control): 限制对某些属性的访问。
const sensitiveData = {
username: 'admin',
password: 'secret_password', // 敏感信息
role: 'administrator'
};
const accessControlHandler = {
get: function(target, property, receiver) {
if (property === 'password') {
return undefined; // 隐藏密码
}
return Reflect.get(target, property, receiver);
}
};
const protectedData = new Proxy(sensitiveData, accessControlHandler);
console.log(protectedData.username); // 输出:admin
console.log(protectedData.password); // 输出:undefined (密码被隐藏了)
代码解释:
get
陷阱拦截了属性读取操作。- 当读取
password
属性时,get
陷阱会返回undefined
,隐藏密码。
- 日志记录 (Logging): 记录对数据的操作。
const myObject = {
name: 'Example',
value: 10
};
const loggingHandler = {
get: function(target, property, receiver) {
console.log(`读取属性:${property}`);
return Reflect.get(target, property, receiver);
},
set: function(target, property, value, receiver) {
console.log(`设置属性:${property} = ${value}`);
Reflect.set(target, property, value, receiver);
return true;
}
};
const loggedObject = new Proxy(myObject, loggingHandler);
loggedObject.name = 'New Example'; // 输出:设置属性:name = New Example
console.log(loggedObject.value); // 输出:读取属性:value 10
代码解释:
get
和set
陷阱分别拦截属性读取和设置操作。- 每次操作都会记录日志。
五、Proxy的优缺点:别光听好,也得知道坑
优点:
- 更灵活: 可以拦截几乎所有对象操作,比
Object.defineProperty
更强大。 - 性能更好: Vue 3.x 用
Proxy
替代Object.defineProperty
,提高了性能。 - 非侵入性: 不需要修改原始对象,直接代理即可。
- 支持代理整个对象: 可以代理数组、函数等。
缺点:
- 兼容性: IE 不支持,需要做兼容处理(polyfill)。
- 性能损耗: 毕竟多了一层代理,会带来一定的性能损耗,但通常可以忽略不计。
- 调试困难: 由于存在代理,调试时可能会比较麻烦。
六、总结:Proxy,你的代码小助手
Proxy
是一个非常强大的工具,可以让你更灵活地控制对象的操作。 在 ORM 中,它可以实现延迟加载、数据验证、字段转换等功能。 在数据拦截中,它可以实现数据劫持、访问控制、日志记录等功能。
当然,Proxy
也有一些缺点,比如兼容性和性能损耗。 但总的来说,Proxy
是一个值得学习和使用的技术。 掌握它,你就可以写出更优雅、更强大的代码。
好了,今天的讲座就到这里。 希望大家有所收获,也希望大家在实际项目中多多使用 Proxy
,让你的代码更上一层楼! 散会!