JavaScript 代理的超能力
今天我们将会学习ECMAScript 6 代理。我们将会在本篇文章中涵盖以下所有内容:
- 什么是代理
- 代理在运转
- 谁用代理
- 用例和样例
- 资源
我们开始吧 :)
什么是代理
如同MDN 网站陈述的那样:
代理
对象能让你为另一个对象创建一个代理,该代理可以拦截和重新定义那个对象的基本操作。
他们通过说它创建了一个代理来解释什么是代理,这有点可笑。当然,他们并没有说错,但是我们可以简化这种说法,使得它可以更友好:
通过这个代理对象,你可以包装目标对象,并通过这样做,我们可以拦截和重新定义那个对象的基本操作。
基本上,这意味着我们将要获取一个对象,用一个 代理 包装它,这将允许我们创建一个”隐藏” 的后门,并控制对所需对象的所有访问。
小提示: 代理 也是一种软件设计模式,你绝对应该了解它(Wikipedia Link).
创建 代理
需要两个参数:
target
: 你想包装的原始对象 (代理)handler
: 一个对象,该对象定义了哪些操作将会被拦截以及如何重新定义也可以称为“陷阱”的拦截操作。
const target = {
message1: "hello",
message2: "everyone"
};
const handler = {};
const proxy = new Proxy(target, handler);
大多数浏览器都支持代理,但是很少有不支持代理的旧浏览器(当然是IE),您可以查看完整列表 这里 。Google有一个 polyfill 用于代理,但是它不支持所有代理功能。
现在我们已经知道什么是 代理 了,接下来看下我们可以用它做什么。
数据操作代理
假想自己是银行或者一个多疑的老伴,我们想以通知的形式知晓每次访问银行账户时的余额情况。这里将使用最最简单的 handler 实现操作/捕获数据,详见:get
const bankAccount = {
balance: 2020,
name: 'Georgy Glezer'
};
const handler = {
get: function(target, prop, receiver) {
if (prop === 'balance') {
console.log(`Current Balance Of: ${target.name} Is: ${target.balance}`);
}
return target[prop];
}
};
const wrappedBankAcount = new Proxy(bankAccount, handler);
wrappedBankAcount.balance; // 获取余额
// 输出:
// Current Balance Of: Georgy Glezer Is: 2020
// 2020
在上面的示例中,我们定义了一个银行账户对象,其中包含了我的名字和 2020 年的余额。
handler 对象在这里实现了 get 操作的捕获,它被定义为一个函数,这个函数具有三个参数和 get 操作的返回值。
- target: 正在访问的对象(我们包装的那个)
- prop:在示例中被访问的属性 - 这里是 “balance”
- receiver: 其他代理或从代理继承的对象
我们在函数中加了一个条件判断,如果属性名称是 “balance”,将会发一个通知(console.log),通知包含余额以及当前用户名,并返回访问属性 “balance”。
正如输出所示,每当 “balance” 属性被访问,我们就可以通过设置了 get 方法的 Proxy 方便的收到访问通知。
接下来继续实现我们的银行构想,要求是每次有人从银行账户取钱时都能被通知到。另外还有一个限制是银行不允许负余额的存在。这次我们使用 set 来实现。
const bankAccount = {
balance: 2020,
name: 'Georgy Glezer'
};
const handler = {
set: function (obj, prop, value) {
console.log(`Current Balance: ${obj.balance}, New Balance: ${value}`);
if (value < 0) {
console.log(`We don't allow Negative Balance!`);
return false;
}
obj[prop] = value;
return true;
}
};
const wrappedBankAcount = new Proxy(bankAccount, handler);
wrappedBankAcount.balance -= 2000; // 取钱
console.log(wrappedBankAcount.balance);
wrappedBankAcount.balance -= 50; // 取钱
console.log(wrappedBankAcount.balance);
// 输出:
// Current Balance: 2020, New Balance: 20
// 20
// Current Balance: 20, New Balance: -30
// We don't allow Negative Balance!
// 20
在上述示例中,我们在取钱后获知了当前余额和新余额,并且在新余额为负时终止取钱操作。
我们使用了 set 函数捕获写操作,如果更新操作成功返回 true,否则返回 false。此函数接受如下参数:
- target: 正在访问的对象(我们包装的那个)
- prop: 在示例中被访问的属性 - 这里是 “balance”
- value: 将被更新的新值
- receiver: 最初指向的对象,通常是 Proxy 对象本身。不过
set
处理函数除也能通过原型链或者其他方式间接调用,所以此参数也可能是别的对象。
可以看到这与 get 非常相似,不过只有一个额外的参数,用于告诉你新设置的值。
这两个处理函数是最常见的,如果想了解全部的操作,可以看下 这个 。
哪些项目使用代理
很多流行的库都会使用这项技术,比如:
除此之外还有更多的项目……其大多数都利用了强大的代理技术为我们提供了很棒的库。
范例和用法
我们已知代理可用于:
- 实时记录(通知到银行)
- 做验证(阻止负余额的更新)
缓存
这里将再次使用 get 操作符并将 “dollars” 属性分配给对象。一旦访问 “dollars” 属性,将会自动计算我们的余额对应多少美元。由于计算可能是一项非常耗时的操作,所以我们希望能尽可能命中缓存进而加速响应。
const bankAccount = {
balance: 10,
name: 'Georgy Glezer',
get dollars() {
console.log('Calculating Dollars');
return this.balance *3.43008459;
}
};
let cache = {
currentBalance: null,
currentValue: null
};
const handler = {
get: function (obj, prop) {
if (prop === 'dollars') {
let value = cache.currentBalance !== obj.balance ? obj[prop] : cache.currentValue;
cache.currentValue = value;
cache.currentBalance = obj.balance;
return value;
}
return obj[prop];
}
};
const wrappedBankAcount = new Proxy(bankAccount, handler);
console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);
// 输出:
// Calculating Dollars
// 34.3008459
// 34.3008459
// 34.3008459
// 34.3008459
如示例中所见,我们创建了一个缓存对象,其中包含了当前的银行余额和同价值的美金余额。每当有人访问 “dollars” 属性时,我们将只进行第一次计算,然后将其缓存。
DOM 操作
如果希望每当余额发生变化时,都能实时更新屏幕上的信息。可以为此属性使用 set 操作符,然后每当该值发生变化时,都同步更新屏幕中的 DOM 元素。
const bankAccount = {
balance: 2020,
name: "Georgy Glezer",
get text() {
return `${this.name} Balance Is: ${this.balance}`;
}
};
const objectWithDom = (object, domId) => {
const handler = {
set: function (obj, prop, value) {
obj[prop] = value;
document.getElementById(domId).innerHTML = obj.text;
return true;
}
};
return new Proxy(object, handler);
};
// 创建 dom 元素,其 id 为 bank-account
const wrappedBankAccount = objectWithDom(bankAccount, "bank-account");
wrappedBankAccount.balance = 26;
wrappedBankAccount.balance = 100000;
在这里我们创建了一个辅助函数以便存储 DOM 元素的 ID ,还在 set 操作符中添加了一行简单的代码更新 DOM 元素。超简单是吧?下面我们一起看下最终效果 :)
总结
总而言之,我们了解了 ECMAScript 6 的代理,包括如何使用它,以及使用它可以实现什么需求。我觉得代理是一个很强大的工具,我们可将其用于多种场景,并只选择适合业务的方法即可。:)
如果你喜欢这篇文章,欢迎一键三连哟 👏
参考资料
编码技能提升
欢迎加入我们社区!订阅我们的 YouTube 频道 或加入 Skilled.dev 编码面试小课。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
vue3的核心之一