【问题标题】:Javascript: operator overloadingJavascript:运算符重载
【发布时间】:2021-08-01 11:46:35
【问题描述】:

我已经使用 JavaScript 工作了几天,并且已经到了要为我定义的对象重载运算符的地步。

在 google 上搜索了一段时间后,您似乎无法正式执行此操作,但仍有一些人声称执行此操作的方式很冗长。

基本上我已经创建了一个 Vector2 类并希望能够执行以下操作:

var x = new Vector2(10,10);
var y = new Vector2(10,10);

x += y; //This does not result in x being a vector with 20,20 as its x & y values.

相反,我必须这样做:

var x = new Vector2(10,10);
var y = new Vector2(10,10);

x = x.add(y); //This results in x being a vector with 20,20 as its x & y values. 

我可以采取一种方法来重载 Vector2 类中的运算符吗?因为这看起来很丑。

【问题讨论】:

标签: javascript operators operator-overloading


【解决方案1】:

如您所见,JavaScript 不支持运算符重载。最接近的方法是实现 toString (当实例需要被强制为字符串时将被调用)和 valueOf (将被调用以将其强制为一个数字,例如在使用 @987654328 时@ 用于加法,或者在许多情况下将其用于连接,因为+ 尝试在连接之前进行加法),这是非常有限的。结果,它们都不允许您创建 Vector2 对象。同样,Proxy(在 ES2015 中添加)允许您拦截各种对象操作(包括属性访问),但同样不会让您控制 +=Vector 实例上的结果。


但是,对于提出此问题并希望得到字符串或数字(而不是 Vector2)的人,这里是 valueOftoString 的示例。这些示例演示运算符重载,只是利用 JavaScript 的内置处理转换为原语:

valueOf

此示例将对象的val 属性的值加倍以响应被强制转换为原语,例如通过+

function Thing(val) {
    this.val = val;
}
Thing.prototype.valueOf = function() {
    // Here I'm just doubling it; you'd actually do your longAdd thing
    return this.val * 2;
};

var a = new Thing(1);
var b = new Thing(2);
console.log(a + b); // 6 (1 * 2 + 2 * 2)

或者使用 ES2015 的 class:

class Thing {
    constructor(val) {
      this.val = val;
    }
    valueOf() {
      return this.val * 2;
    }
}

const a = new Thing(1);
const b = new Thing(2);
console.log(a + b); // 6 (1 * 2 + 2 * 2)

或者只有对象,没有构造函数:

var thingPrototype = {
    valueOf: function() {
      return this.val * 2;
    }
};

var a = Object.create(thingPrototype);
a.val = 1;
var b = Object.create(thingPrototype);
b.val = 2;
console.log(a + b); // 6 (1 * 2 + 2 * 2)

toString

此示例将对象的val 属性的值转换为大写,以响应被强制转换为原语,例如通过+

function Thing(val) {
    this.val = val;
}
Thing.prototype.toString = function() {
    return this.val.toUpperCase();
};

var a = new Thing("a");
var b = new Thing("b");
console.log(a + b); // AB

或者使用 ES2015 的 class:

class Thing {
    constructor(val) {
      this.val = val;
    }
    toString() {
      return this.val.toUpperCase();
    }
}

const a = new Thing("a");
const b = new Thing("b");
console.log(a + b); // AB

或者只有对象,没有构造函数:

var thingPrototype = {
    toString: function() {
      return this.val.toUpperCase();
    }
};

var a = Object.create(thingPrototype);
a.val = "a";
var b = Object.create(thingPrototype);
b.val = "b";
console.log(a + b); // AB

【讨论】:

  • 虽然 JS 本身不支持它,但如今使用自定义功能扩展 JS 并转译回普通 JS 是很常见的,例如,SweetJS 旨在解决这个问题。
  • Date 类上的比较运算符是否使用valueOf 将日期隐式转换为数字?例如,您可以执行date2 > date1,如果date2 是在date1 之后创建的,则为true。
  • @SeanLetendre:是的。 ><>=<=(但不包括 =====!=!==)使用带有提示的 Abstract Relational Comparison 操作,它使用 @98765434@ “数字”。在Date 对象上,这会导致getTime 返回的数字(毫秒-since-The-Epoch 值)。
  • 也可以使用Proxy 对象来overload the [] operator
  • @AndersonGreen - 这并没有真正使 operator 超载(例如,它也会影响.),但是是的,您可以拦截各种对象操作(包括属性访问) . += 对我们没有帮助,但是...
【解决方案2】:

我编写了一个库,该库利用一堆邪恶的 hack 来在原始 JS 中执行此操作。它允许这样的表达式。

  • 复数:

    >> Complex()({r: 2, i: 0} / {r: 1, i: 1} + {r: -3, i: 2}))

    <- {r: -2, i: 1}

  • 自动微分:

    f(x) = x^3 - 5x:

    >> var f = x => Dual()(x * x * x - {x:5, dx:0} * x);

    现在将它映射到一些值上:

    >> [-2,-1,0,1,2].map(a=>({x:a,dx:1})).map(f).map(a=>a.dx)

    <- [ 7, -2, -5, -2, 7 ]

    f'(x) = 3x^2 - 5.

  • 多项式:

    >> Poly()([1,-2,3,-4]*[5,-6]).map((c,p)=>''+c+'x^'+p).join(' + ')

    <- "5x^0 + -16x^1 + 27x^2 + -38x^3 + 24x^4"

对于您的特定问题,您可以使用库定义一个 Vector2 函数(或者更短的函数),然后编写 x = Vector2()(x + y);

https://gist.github.com/pyrocto/5a068100abd5ff6dfbe69a73bbc510d7

【讨论】:

    【解决方案3】:

    可以将两个数字合二为一来进行矢量数学运算。在解释它的工作原理之前,让我先展示一个示例:

    let a = vec_pack([2,4]);
    let b = vec_pack([1,2]);
    
    let c = a+b; // Vector addition
    let d = c-b; // Vector subtraction
    let e = d*2; // Scalar multiplication
    let f = e/2; // Scalar division
    
    console.log(vec_unpack(c)); // [3, 6]
    console.log(vec_unpack(d)); // [2, 4]
    console.log(vec_unpack(e)); // [4, 8]
    console.log(vec_unpack(f)); // [2, 4]
    
    if(a === f) console.log("Equality works");
    if(a > b) console.log("Y value takes priority");
    

    我使用的事实是,如果您将两个数字移位 X 次,然后在将它们移回之前添加或减去它们,您将获得与开始时没有移动它们相同的结果。类似地,标量乘法和除法对移位值对称地工作。

    一个 JavaScript 数字有 52 位整数精度(64 位浮点数),所以我将一个数字打包到更高的可用 26 位中,一个到较低的可用位。因为我想支持带符号的数字,所以代码变得有点混乱。

    function vec_pack(vec){
        return vec[1] * 67108864 + (vec[0] < 0 ? 33554432 | vec[0] : vec[0]);
    }
    
    function vec_unpack(number){
        switch(((number & 33554432) !== 0) * 1 + (number < 0) * 2){
            case(0):
                return [(number % 33554432),Math.trunc(number / 67108864)];
            break;
            case(1):
                return [(number % 33554432)-33554432,Math.trunc(number / 67108864)+1];
            break;
            case(2):
                return [(((number+33554432) % 33554432) + 33554432) % 33554432,Math.round(number / 67108864)];
            break;
            case(3):
                return [(number % 33554432),Math.trunc(number / 67108864)];
            break;
        }
    }
    

    我能看到的唯一缺点是 x 和 y 必须在 +-3300 万范围内,因为它们必须分别在 26 位以内。

    【讨论】:

    • vec_pack的定义在哪里?
    • @Disgusting 嗯,抱歉,好像我忘记添加了...现在已修复 :)
    【解决方案4】:

    虽然不是问题的确切答案,但可以使用 ES6 符号实现一些 python __magic__ 方法

    [Symbol.toPrimitive]() 方法不允许您暗示调用Vector.add(),但允许您使用诸如Decimal() + int 之类的语法。

    class AnswerToLifeAndUniverseAndEverything {
        [Symbol.toPrimitive](hint) {
            if (hint === 'string') {
                return 'Like, 42, man';
            } else if (hint === 'number') {
                return 42;
            } else {
                // when pushed, most classes (except Date)
                // default to returning a number primitive
                return 42;
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      我们可以使用类似 React 的 Hooks 在每次迭代时使用 valueOf 方法的不同值来评​​估箭头函数。

      const a = Vector2(1, 2) // [1, 2]
      const b = Vector2(2, 4) // [2, 4]    
      const c = Vector2(() => (2 * a + b) / 2) // [2, 4]
      // There arrow function will iterate twice
      // 1 iteration: method valueOf return X component
      // 2 iteration: method valueOf return Y component
      

      const Vector2 = (function() {
        let index = -1
        return function(x, y) {
          if (typeof x === 'function') {
            const calc = x
            index = 0, x = calc()
            index = 1, y = calc()
            index = -1
          }
          return Object.assign([x, y], {
            valueOf() {
              return index == -1 ? this.toString() : this[index]
            },
            toString() {
              return `[${this[0]}, ${this[1]}]`
            },
            len() {
              return Math.sqrt(this[0] ** 2 + this[1] ** 2)
            }
          })
        }
      })()
      
      const a = Vector2(1, 2)
      const b = Vector2(2, 4)
      
      console.log('a = ' + a) // a = [1, 2]
      console.log(`b = ${b}`) // b = [2, 4]
      
      const c = Vector2(() => (2 * a + b) / 2) // [2, 4]
      a[0] = 12
      const d = Vector2(() => (2 * a + b) / 2) // [13, 4]
      const normalized = Vector2(() => d / d.len()) // [0.955..., 0.294...]
      
      console.log(c, d, normalized)

      @js-basics/vector 对 Vector3 使用相同的想法。

      【讨论】:

        【解决方案6】:

        有趣的也是实验性operator-overloading-js。它仅在定义的上下文(回调函数)中进行重载。

        【讨论】:

        • 对于任何对它的工作原理感兴趣的人,它会解析函数的字符串表示并在运行时构建一个新函数,用函数调用替换运算符。
        【解决方案7】:

        实际上,有一种 JavaScript 变体确实支持运算符重载。 ExtendScript 是 Photoshop 和 Illustrator 等 Adob​​e 应用程序使用的脚本语言,它确实具有运算符重载。在里面,你可以写:

        Vector2.prototype["+"] = function( b )
        {
          return new Vector2( this.x + b.x, this.y + b.y );
        }
        
        var a = new Vector2(1,1);
        var b = new Vector2(2,2);
        var c = a + b;
        

        这在“Adobe Extendscript JavaScript 工具指南”(当前link here)中有更详细的描述。该语法显然是基于 ECMAScript 标准的(现已长期放弃的)草案。

        【讨论】:

        • ExtendScript != JavaScript
        • 为什么 ExtendScript 答案被低估,而 PaperScript 答案被赞成?恕我直言,这个答案也很好。
        【解决方案8】:

        仅供参考,paper.js 通过创建 PaperScript 解决了这个问题,PaperScript 是一个独立的、有范围的 javascript,具有向量的运算符重载,然后将其处理回 javascript。

        但paperscript文件需要具体指定和处理。

        【讨论】:

          【解决方案9】:

          作为 T.J.说,你不能在 JavaScript 中重载运算符。但是,您可以利用valueOf 函数编写一个看起来比每次都使用add 之类的函数更好的hack,但对x 和y 在0 和MAX_VALUE 之间的向量施加了约束。代码如下:

          var MAX_VALUE = 1000000;
          
          var Vector = function(a, b) {
              var self = this;
              //initialize the vector based on parameters
              if (typeof(b) == "undefined") {
                  //if the b value is not passed in, assume a is the hash of a vector
                  self.y = a % MAX_VALUE;
                  self.x = (a - self.y) / MAX_VALUE;
              } else {
                  //if b value is passed in, assume the x and the y coordinates are the constructors
                  self.x = a;
                  self.y = b;
              }
          
              //return a hash of the vector
              this.valueOf = function() {
                  return self.x * MAX_VALUE + self.y;
              };
          };
          
          var V = function(a, b) {
              return new Vector(a, b);
          };
          

          然后你可以写出这样的方程:

          var a = V(1, 2);            //a -> [1, 2]
          var b = V(2, 4);            //b -> [2, 4]
          var c = V((2 * a + b) / 2); //c -> [2, 4]
          

          【讨论】:

          • 您基本上只是为 OP 的 add 方法编写了代码......他们不想做的事情。
          • @IanBrindley OP 想要重载一个运算符,这显然意味着他计划编写这样一个函数。 OP 担心必须调用“add”,这是不自然的。在数学上,我们用+ 符号表示向量加法。这是一个很好的答案,展示了如何避免为准数字对象调用不自然的函数名。
          • @Kittsil 问题表明我已经在使用添加功能。虽然上面的函数根本不是一个坏函数,但它并没有解决问题,所以我同意 Ian 的观点。
          • 到目前为止,这是唯一可能的方法。 + 运算符的唯一灵活性是能够返回 Number 作为操作数之一的替换。因此,任何适用于Object 实例的添加功能必须始终将对象编码为Number,并最终对其进行解码。
          • 请注意,这将在将两个向量相乘时返回意外结果(而不是给出错误)。坐标也必须是整数。
          猜你喜欢
          • 2010-12-10
          相关资源
          最近更新 更多