发布于 2026-01-06 0 阅读
0

使用 ES6 代理增强对象的 3 种方法

使用 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