使用 ES6 代理增强对象的 3 种方法
使用 ES6 代理增强对象的 3 种方法
原文发布于:https://blog.logrocket.com/use-es6-proxies-to-enhance-your-objects/
使用 ES6 代理增强对象的 3 种方法
我最喜欢的编程方面之一是元编程,它指的是利用编程语言本身来修改其基本构建模块的能力。开发者使用这种技术来增强语言,甚至在某些情况下,还会创建新的自定义语言,称为领域特定语言(简称DSL)。
许多语言已经提供了深层次的元编程,但 JavaScript 缺少一些关键方面。
没错,JavaScript 的确非常灵活,允许你对这门语言进行相当大的拓展,例如在运行时为对象添加属性,或者通过传递不同的函数作为参数来轻松增强函数的行为。但即便如此,仍然存在一些限制,而新的代理机制使我们能够突破这些限制。
在本文中,我想介绍三种可以使用代理来增强对象功能的方法。希望读完本文后,您能够扩展我的代码,并将其应用到您自己的需求中!
代理服务器是如何工作的?简要介绍
代理本质上就是将你的对象或函数包装在一组陷阱中,一旦这些陷阱被触发,你的代码就会被执行。很简单,对吧?
我们可以使用的陷阱有:
| 陷阱 | 描述 |
|---|---|
| 获取原型 | 当您在自己的对象上调用同名方法时触发。 |
| 设置原型 | 与之前相同,但针对的是这种特定方法。 |
| 可扩展 | 当我们尝试了解一个对象是否可以扩展时(即在运行时向其添加新属性),会触发此事件。 |
| 阻止扩展 | 与之前相同,但针对的是这种特定方法(顺便说一句,它会忽略你在运行时添加到对象的任何新属性)。 |
| 获取自身属性描述符 | 此方法通常返回给定对象属性的描述符对象。当使用此方法时,会触发此陷阱。 |
| 定义属性 | 当调用此方法时执行。 |
| 有 | 当我们使用`in`运算符时(例如使用 `[] if('value' in array)`),就会触发此陷阱。这非常有趣,因为您不仅限于为数组添加此陷阱,还可以将其扩展到其他对象。 |
| 得到 | 非常简单,当您尝试访问属性值时触发(即yourObject.prop)。 |
| 放 | 与上面的例子相同,但会在你设置属性值时触发。 |
| 删除属性 | 基本上,这是使用该运算符时触发的陷阱delete。 |
| ownKeys | 当您对对象使用getOwnPropertyNamesand方法时触发。getOwnPropertySymbols |
| 申请 | 调用函数时触发。我们会重点关注这一点,请耐心等待。 |
| 构造 | 当您使用运算符实例化一个新对象时触发new。 |
这些都是常见的陷阱,您可以查阅Mozilla 的 Web 文档了解更多详情,因为本文将重点介绍其中的一部分。
也就是说,创建新代理的方式,或者换句话说,用代理包装对象或函数调用的方式,大致如下:
let myString = new String("hi there!")
let myProxiedVar = new Proxy(myString, {
has: function(target, key) {
return target.indexOf(key) != -1;
}
})
console.log("i" in myString)
// false
console.log("i" in myProxiedVar)
//true
这就是代理的基础,我稍后会展示更复杂的示例,但它们都基于相同的语法。
代理与反射
但在开始看例子之前,我想先快速解答一下这个问题,因为这个问题经常被问到。ES6 不仅引入了代理,还引入了 ` [Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect) Object` 对象,乍一看,它和代理的作用完全一样,对吧?
主要的困惑在于,大多数文档都声称它Reflect与我们上面看到的代理处理程序(即陷阱)具有相同的方法。虽然这没错,它们之间确实存在一一对应的关系,但该Reflect对象及其方法的行为更类似于Object全局对象。
例如,以下代码:
const object1 = {
x: 1,
y: 2
};
console.log(Reflect.get(object1, 'x'));
返回值将为 1,就像您直接访问该属性一样。因此,您无需改变预期行为,只需使用不同的(在某些情况下,更动态的)语法执行即可。
改进#1:动态属性访问
现在我们来看一些例子。首先,我想向您展示如何为检索属性值的操作添加额外的功能。
我的意思是,假设你有一个对象,例如:
class User {
constructor(fname, lname) {
this.firstname = fname
this.lastname = lname
}
}
你可以轻松获取名字或姓氏,但无法一次性获取全名。如果你想获取全大写的名字,则需要链式调用方法。这并非什么问题,在 JavaScript 中就是这样做的:
let u = new User("fernando", "doglio")
console.log(u.firstname + " " + u.lastname)
//would yield: fernando doglio
console.log(u.firstname.toUpperCase())
//would yield: FERNANDO
但是,借助代理,你可以让你的代码更具声明性。想想看,如果你的对象能够支持诸如此类的语句,那会怎么样:
let u = new User("fernando", "doglio")
console.log(u.firstnameAndlastname)
//would yield: fernando doglio
console.log(u.firstnameInUpperCase)
//would yield: FERNANDO
当然,这样做的目的是将这种通用行为添加到任何类型的对象中,避免手动创建额外的属性,从而污染对象的命名空间。
这时代理就派上用场了。如果我们对对象进行包装,并为获取属性值的操作设置陷阱,我们就可以拦截属性名称并对其进行解释,从而获得所需的行为。
以下代码可以实现这个功能:
function EnhanceGet(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
if(target.hasOwnProperty(prop)) {
return target[prop]
}
let regExp = /([a-z0-9]+)InUpperCase/gi
let propMatched = regExp.exec(prop)
if(propMatched) {
return target[propMatched[1]].toUpperCase()
}
let ANDRegExp = /([a-z0-9]+)And([a-z0-9]+)/gi
let propsMatched = ANDRegExp.exec(prop)
if(propsMatched) {
return [target[propsMatched[1]], target[propsMatched[2]]].join(" ")
}
return "not found"
}
});
}
我们实际上是在为陷阱设置一个代理get,并使用正则表达式来解析属性名称。首先,我们会检查名称是否与实际属性匹配,如果匹配,则直接返回。然后,我们会检查正则表达式的匹配项,当然,我们会捕获实际的名称,以便从对象中获取该值,并进行进一步处理。
现在你可以将该代理与你自己的任何对象一起使用,并且属性获取器将得到增强!
增强功能#2:针对无效属性名称的自定义错误处理
接下来是另一个虽小但很有意思的改进。当你尝试访问对象上不存在的属性时,不会直接报错,JavaScript 在这方面比较宽容。你只会得到一个undefined空值,而不是该属性的实际值。
如果我们不想获得这种行为,而是想自定义返回值,甚至因为开发人员试图访问不存在的属性而抛出异常,该怎么办?
我们完全可以使用代理来实现这一点,方法如下:
function CustomErrorMsg(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
if(target.hasOwnProperty(prop)) {
return target[prop]
}
return new Error("Sorry bub, I don't know what a '" + prop + "' is...")
}
});
}
现在,这段代码会导致以下行为:
> pa = CustomErrorMsg(a)
> console.log(pa.prop)
Error: Sorry bub, I don't know what a 'prop' is...
at Object.get (repl:7:14)
at repl:1:16
at Script.runInThisContext (vm.js:91:20)
at REPLServer.defaultEval (repl.js:317:29)
at bound (domain.js:396:14)
at REPLServer.runBound [as eval] (domain.js:409:12)
at REPLServer.onLine (repl.js:615:10)
at REPLServer.emit (events.js:187:15)
at REPLServer.EventEmitter.emit (domain.js:442:20)
at REPLServer.Interface._onLine (readline.js:290:10)
我们可以采取更极端的做法,就像我之前提到的那样,比如:
function HardErrorMsg(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
if(target.hasOwnProperty(prop)) {
return target[prop]
}
throw new Error("Sorry bub, I don't know what a '" + prop + "' is...")
}
});
}
现在,我们正在强制要求开发者在使用您的对象时更加谨慎:
> a = {}
> pa2 = HardErrorMsg(a)
> try {
... console.log(pa2.property)
} catch(e) {
... console.log("ERROR Accessing property: ", e)
}
ERROR Accessing property: Error: Sorry bub, I don't know what a 'property' is...
at Object.get (repl:7:13)
at repl:2:17
at Script.runInThisContext (vm.js:91:20)
at REPLServer.defaultEval (repl.js:317:29)
at bound (domain.js:396:14)
at REPLServer.runBound [as eval] (domain.js:409:12)
at REPLServer.onLine (repl.js:615:10)
at REPLServer.emit (events.js:187:15)
at REPLServer.EventEmitter.emit (domain.js:442:20)
at REPLServer.Interface._onLine (readline.js:290:10)
使用代理,你完全可以向数据集中添加验证,确保为属性分配正确的数据类型。
利用上面所示的基本行为,您可以做很多事情,从而根据您的特定意愿来塑造 JavaScript。
改进#3:基于方法名称的动态行为
最后一个例子与第一个例子类似。之前我们可以通过属性名添加额外的功能(例如使用“InUpperCase”结尾),现在我想对方法调用也做同样的事情。这样一来,我们不仅可以通过在方法名后添加额外信息来扩展基本方法的行为,还可以接收与这些额外信息关联的参数。
我举个例子来说明我的意思:
myDbModel.findById(2, (err, model) => {
//....
})
如果您之前使用过数据库 ORM(例如 Sequelize 或 Mongoose),那么这段代码对您来说应该很熟悉。该框架能够根据您设置模型的方式推测 ID 字段的名称。但是,如果您想将其扩展到类似这样的功能呢?
myDbModel.findByIdAndYear(2, 2019, (err, model) => {
//...
})
更进一步:
myModel.findByNameAndCityAndCountryId("Fernando", "La Paz", "UY", (err, model) => {
//...
})
我们可以使用代理来增强对象,使其能够实现这种行为,从而在无需手动添加方法的情况下提供扩展功能。此外,如果数据库模型足够复杂,即使通过编程方式,所有可能的组合也难以全部添加,最终对象会包含太多我们根本用不到的方法。这样,我们就能确保只有一个能够处理所有组合的通用方法。
在示例中,为了简单起见,我将创建一个虚拟的 MySQL 模型,仅使用一个自定义类:
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'user',
password : 'pwd',
database : 'test'
});
connection.connect();
class UserModel {
constructor(c) {
this.table = "users"
this.conn = c
}
}
构造函数中的属性仅供内部使用,表格可以包含您想要的所有列,这没有任何区别。
let Enhacer = {
get : function(target, prop, receiver) {
let regExp = /findBy((?:And)?[a-zA-Z_0-9]+)/g
return function() { //
let condition = regExp.exec(prop)
if(condition) {
let props = condition[1].split("And")
let query = "SELECT * FROM " + target.table + " where " + props.map( (p, idx) => {
let r = p + " = '" + arguments[idx] + "'"
return r
}).join(" AND ")
return target.conn.query(query, arguments[arguments.length - 1])
}
}
}
}
这只是处理程序,我稍后会演示如何使用它,但首先有几点需要注意:
- 注意正则表达式。我们在之前的例子中也用过,但那些例子比较简单。这里我们需要一种方法来捕获重复的模式:findBy + propName + 并根据需要重复多次。
- 通过
map调用,我们确保将每个属性名称映射到我们接收到的值。我们使用arguments对象获取实际值。这就是为什么我们返回的函数不能是箭头函数(箭头函数无法arguments访问对象)。 - 我们还使用了目标对象的
table属性及其子conn属性。正如你所预期的,目标对象就是我们的对象,这也是为什么我们在构造函数中定义了这些属性的原因。为了保持代码的通用性,这些属性需要来自外部。 - 最后,我们调用这个
query方法时使用了两个参数,并且我们假设伪方法接收到的最后一个参数就是实际的回调函数。这样我们就可以获取它并传递给它。
以上内容的总结就是:我们将方法名称转换为 SQL 查询,并使用实际query方法执行该查询。
以下是如何使用上述代码:
let eModel = new Proxy(new UserModel(connection), Enhacer) //create the proxy here
eModel.findById("1", function(err, results) { //simple method call with a single parameter
console.log(err)
console.log(results)
})
eModel.findByNameAndId('Fernando Doglio', 1, function(err, results) { //extra parameter added
console.log(err)
console.log(results)
console.log(results[0].name)
})
就是这样,之后就可以像往常一样使用结果了,无需任何额外操作。
结论
本文到此结束,希望它能帮助你更好地理解代理及其用途。现在,尽情发挥你的想象力,用代理创建你自己的 JavaScript 版本吧!
下次见!
文章来源:https://dev.to/deleteman123/3-ways-to-use-es6-proxies-to-enhance-your-objects-mfg