js元编程:妙用proxy实现add[1][2][3] + 4

457 阅读4分钟

js元编程相关文章

js元编程: Symbol&Reflect&Proxy
js基石之Symbol值
js元编程:妙用proxy实现add[1][2][3] + 4

什么是元编程:

元编程是指编写可以操作或改变其他程序的程序。

元编程可以改变 JavaScript 的一些基本操作的行为。

就算不理解元编程的概念也不重要,在js里面元编程主要与这三个对象有关。

Symbol:通过覆写内置的Symbol值等方法 更改js语言内部的基本操作

Reflect:可以获取语言内部的基本操作

Proxy:通过钩子函数 拦截&改变 js语言的基本操作

因此也可以理解为这个系列文章主要是讲解这三个api的。

题目如下

const res = add[1][2][3] + 4
console.log(res) // 10

使用js如何实现上面的逻辑…

先有问题再有答案

  1. 这个问题可以被拆分为哪些子任务?
  2. 链式调用如何实现?
  3. 累加后的结果如何保存
  4. 对象和数字类型做加法运算js是如何处理的

这个问题 add[1][2][3] + 4 按照 +运算符可以被分为三部分。

第一个:add[1][2][3] 链式调用并结果累加
第二个:+ 在对象和数字的转化
第三个:最原始的加法运算得出结果.

对象和数字如何计算

因为add返回的一定是一个对象 否则链式调用无法实现
所以这里涉及到了js引用类型与基础类型的转换 具体可以参考之前的文章: js基石之数据类型三:类型转换

这里列下关键步骤:
对象在转换到数字或字符串时 逻辑是相似的 只是顺序会有不同。

  1. 判断对象是否实现了Symbol.toPrimitive方法。
    如果实现了则按照这个方法返回,如果对象没有实现 则走js内部的Symbol.toPrimitive方法。
    内部Symbol.toPrimitive大概逻辑如下:
function ToPrimitive(input, preferredType) {
  switch (preferredType) {
    case Number:
      return toNumber(input);
      break;
    case String:
      return toString(input);
      break;
    default:
      return toNumber(input);
  }

  function isPrimitive(value) {
    return value !== Object(value);
  }

  function toString() {
    if (isPrimitive(input.toString())) return input.toString();
    if (isPrimitive(input.valueOf())) return input.valueOf();
    throw new TypeError();
  }

  function toNumber() {
    if (isPrimitive(input.valueOf())) return input.valueOf();
    if (isPrimitive(input.toString())) return input.toString();
    throw new TypeError();
  }
}
  1. 如果是转换为number则调用ToPrimitive(input, 'Number')
  2. 如果是转换为String 则调用ToPrimitive(input, 'String')
  3. 不能转换为原始值走报错逻辑
  4. 可以转换为原始值 则返回当前的原始值 然后在继续运算 后续可能还会将这个原始值在进行类型转换。

所以在我们这个场景下 可以实现如下代码:

const obj = {
    value: 1,
    [Symbol.toPrimitive](){
        return this.value;
    }
};
console.log('add', obj + 10) // add 11

链式求和

add[1]这种使用方式很像数组取值,也就是从一个对象上动态获取某个索引属性…只是这个case下 我们不是直接返回一个数组的内容而是返回原对象,以方便后续的链式调用和取值…

可以利用JavaScript的代理(Proxy)实现动态拦截属性的能力,然后返回代理本身,并计算当前的索引值与存储的结果求和 得出最新值 存储最新值,以此类推。

代码如下:

const add = new Proxy(
  {
    value: 0,
  },
  {
    get(target, property) {
      const newValue = target.value + Number(property);
      return new Proxy({ value: newValue }, this);
    },
  }
);
console.log("test add", add[1][2]["3"][4]); 

截屏2024-04-07 下午2.54.11.png 可以看到 链式调用实现了 并且累加的结果也已经存储在value中。

完整实现:

将上述两个步骤结合在一起 略作改动。

代码如下:

const add = new Proxy(
  {
    value: 0,
  },
  {
    get(target, property) {
      if (property === Symbol.toPrimitive) {
        return () => {
          return target.value;
        };
      }
      if (Number.isNaN(Number(property))) {
        throw new Error("key应该是一个数字");
      }
      const newValue = target.value + Number(property);
      return new Proxy({ value: newValue }, this);
    },
  }
);
console.log("test add", add[1][2]["3"][4] + 4);

当通过add[1]这样的操作访问add时,实际上是在访问代理对象,并试图获取索引"1"。 通过代理的handler的get函数,我们拦截了这个访问,然后将 "1" 转换成数值1并与之前累积的和相加(初始化时为0)。然后,我们基于新的累加和。创建一个新的代理对象,以便能够继续拦截后续的访问。

当执行到add[1][2]["3"][4]时,依次操作实现了1 + 2 + 3 + 4的累加。

最后,当执行加法 + 4 时,JavaScript引擎调用对象的[Symbol.toPrimitive]方法来尝试获取可以进行数学运算的值。在我们的handler中,我们覆盖了对[Symbol.toPrimitive]的访问,使其返回当前累积的和(即10),然后这个值与4相加得到最终结果14。

并且我们也对非索引key做了拦截

console.log("test add", add["a"] + 1);

截屏2024-04-07 下午3.05.07.png

总结

这个场景主要是利用了proxy对get属性的拦截。实际上,JavaScript 的 Proxy 对象为您提供了一种拦截和自定义基本操作(如属性读取、赋值、枚举、函数调用等)的方法。这是十分强大的能力。具有很多创造性的使用方式,可以用来构建高级的抽象、响应性框架、甚至代理和远程对象的实现。所以建议大家熟练掌握proxy相关知识点。