JavaScript内核与高级编程之:`JavaScript`的`Proxy`模式:其在 `ORM` 和数据拦截中的应用。

各位靓仔靓女,晚上好!我是今晚的讲师,大家可以叫我老王。今天咱们聊聊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.getOwnPropertyNamesObject.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 的功能,比如:

  1. 延迟加载 (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.nameuserProxy.age 时,就不会再从数据库加载了,因为数据已经存在了。
  1. 数据验证 (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 的数字。
  • 如果不是,则抛出错误,阻止设置。
  1. 字段转换 (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 在数据拦截方面也有很多用途,比如:

  1. 数据劫持 (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 更新。
  1. 访问控制 (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,隐藏密码。
  1. 日志记录 (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

代码解释:

  • getset 陷阱分别拦截属性读取和设置操作。
  • 每次操作都会记录日志。

五、Proxy的优缺点:别光听好,也得知道坑

优点:

  • 更灵活: 可以拦截几乎所有对象操作,比 Object.defineProperty 更强大。
  • 性能更好: Vue 3.x 用 Proxy 替代 Object.defineProperty,提高了性能。
  • 非侵入性: 不需要修改原始对象,直接代理即可。
  • 支持代理整个对象: 可以代理数组、函数等。

缺点:

  • 兼容性: IE 不支持,需要做兼容处理(polyfill)。
  • 性能损耗: 毕竟多了一层代理,会带来一定的性能损耗,但通常可以忽略不计。
  • 调试困难: 由于存在代理,调试时可能会比较麻烦。

六、总结:Proxy,你的代码小助手

Proxy 是一个非常强大的工具,可以让你更灵活地控制对象的操作。 在 ORM 中,它可以实现延迟加载、数据验证、字段转换等功能。 在数据拦截中,它可以实现数据劫持、访问控制、日志记录等功能。

当然,Proxy 也有一些缺点,比如兼容性和性能损耗。 但总的来说,Proxy 是一个值得学习和使用的技术。 掌握它,你就可以写出更优雅、更强大的代码。

好了,今天的讲座就到这里。 希望大家有所收获,也希望大家在实际项目中多多使用 Proxy,让你的代码更上一层楼! 散会!

发表回复

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