151

I've been working with JavaScript for a few days now and have got to a point where I want to overload operators for my defined objects.

After a stint on google searching for this it seems you can't officially do this, yet there are a few people out there claiming some long-winded way of performing this action.

Basically I've made a Vector2 class and want to be able to do the following:

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.

Instead I'm having to do this:

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. 

Is there an approach I can take to overload operators in my Vector2 class? As this just looks plain ugly.

2

12 Answers 12

171

As you've found, JavaScript doesn't support operator overloading. The closest you can come is to implement toString (which will get called when the instance needs to be coerced to being a string) and valueOf (which will get called to coerce it to a number, for instance when using + for addition, or in many cases when using it for concatenation because + tries to do addition before concatenation), which is pretty limited. Neither lets you create a Vector2 object as a result. Similarly, Proxy (added in ES2015) lets you intercept various object operations (including property access), but again won't let you control the result of += on Vector instances.


For people coming to this question who want a string or number as a result (instead of a Vector2), though, here are examples of valueOf and toString. These examples do not demonstrate operator overloading, just taking advantage of JavaScript's built-in handling converting to primitives:

valueOf

This example doubles the value of an object's val property in response to being coerced to a primitive, for instance via +:

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)

Or with ES2015's 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)

Or just with objects, no constructors:

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

This example converts the value of an object's val property to upper case in response to being coerced to a primitive, for instance via +:

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

Or with ES2015's 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

Or just with objects, no constructors:

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

Sign up to request clarification or add additional context in comments.

5 Comments

While it is not supported within JS proper, it is quite common these days to extend JS with custom features and transpile back to plain JS, for instance, SweetJS is aiming at addressing exactly this problem.
Do the comparison operators on the Date class implicitly convert dates to numbers using valueOf? For example you can do date2 > date1 and it will be true if date2 was created after date1.
@SeanLetendre: Yes. >, <, >=, and <= (but not ==, ===, !=, or !==) use the Abstract Relational Comparison operation, which uses ToPrimitive with hint "number". On a Date object, that results in the number that getTime returns (the milliseconds-since-The-Epoch value).
It's also possible to overload the [] operator using a Proxy object.
@AndersonGreen - That's not really overloading the operator (it also affects ., for instance), but yes, you can intercept various object operations (including property access). That doesn't help us with +=, but...
36

As T.J. said, you cannot overload operators in JavaScript. However you can take advantage of the valueOf function to write a hack which looks better than using functions like add every time, but imposes the constraints on the vector that the x and y are between 0 and MAX_VALUE. Here is the code:

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);
};

Then you can write equations like this:

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]

8 Comments

You have basically just written the code for the OP's add method... Something they didn't want to do.
@IanBrindley The OP wanted to overload an operator, which clearly implies that he planned to write such a function. OP's concern was with having to call "add," which is unnatural; mathematically, we represent vector addition with a + sign. This is a very good answer showing how to avoid calling an unnatural function name for quasi-numeric objects.
@Kittsil The question shows that i'm already using an add function. Although the function above isn't a bad function at all it did not address the question, so i'd agree with Ian.
As of yet this is the only possible way. The only flexibility we have with the + operator is an ability to return a Number as a replacement for one of the operands. Therefore any adding functionality which works Object instances must always encode the object as a Number, and eventually decode it.
Note that this will return an unexpected result (instead of give an error) when multiplying two vector. Also the coordinates must be integer.
|
21

It's possible to do vector math with two numbers packed into one. Let me first show an example before I explain how it works:

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");

I am using the fact that if you bit shift two numbers X times and then add or subtract them before shifting them back, you will get the same result as if you hadn't shifted them to begin with. Similarly scalar multiplication and division works symmetrically for shifted values.

A JavaScript number has 52 bits of integer precision (64 bit floats), so I will pack one number into he higher available 26 bits, and one into the lower. The code is made a bit more messy because I wanted to support signed numbers.

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;
    }
}

The only downside I can see with this is that the x and y has to be in the range +-33 million, since they have to fit within 26 bits each.

2 Comments

Where is the definition of vec_pack?
@Disgusting Hmm sorry, looks like I had forgotten to add that... That is now fixed :)
14

FYI paper.js solves this issue by creating PaperScript, a self-contained, scoped javascript with operator overloading of vectors, which it then processing back into javascript.

But the paperscript files need to be specifically specified and processed as such.

1 Comment

And this comment answers my question. I was reading paper.js code and wondering how they overloaded JS operators to do object math. Thanks!
13

Actually, there is one variant of JavaScript that does support operator overloading. ExtendScript, the scripting language used by Adobe applications such as Photoshop and Illustrator, does have operator overloading. In it, you can write:

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;

This is described in more detail in the "Adobe Extendscript JavaScript tools guide" (current link here). The syntax was apparently based on a (now long abandoned) draft of the ECMAScript standard.

3 Comments

ExtendScript != JavaScript
Why ExtendScript answer is downvoted while PaperScript answer is upvoted? IMHO this answer is also good.
Interestingly, the Graal VM also supports a form of op overloading for JS, which they say it's based on the same draft graalvm.org/latest/reference-manual/js/OperatorOverloading It's not on by default though, needs to be enabled with --experimental-options --js.operator-overloading. There are some differences from the draft, even at syntax level, that they discuss at the end of that page. One limitation is that they don't support overloading of [], i.e. of array indexing.
12

I wrote a library that exploits a bunch of evil hacks to do it in raw JS. It allows expressions like these.

  • Complex numbers:

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

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

  • Automatic differentiation:

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

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

    Now map it over some values:

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

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

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

  • Polynomials:

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

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

For your particular problem, you would define a Vector2 function (or maybe something shorter) using the library, then write x = Vector2()(x + y);

https://reperiendi.wordpress.com/2020/08/30/a-modest-proposal-for-operator-overloading-in-javascript/

2 Comments

The link is dead by now, is the code uploaded anywhere else?
Updated the link, thanks.
11

We can use React-like Hooks to evaluate arrow function with different values from valueOf method on each iteration.

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)

Library @js-basics/vector uses the same idea for Vector3.

1 Comment

So basically "index" is shared amongst all your instances of Vector2, wouldn't it be a problem for say, a promise ?
8

Whilst not an exact answer to the question, it is possible to implement some of the python __magic__ methods using ES6 Symbols

A [Symbol.toPrimitive]() method doesn't let you imply a call Vector.add(), but will let you use syntax such as 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;
        }
    }
}

Comments

7

Interesting is also experimental library operator-overloading-js . It does overloading in a defined context (callback function) only.

2 Comments

For anyone who's interested how this one works, it parses the string representation of the function and builds a new function at runtime that replaces the operators with function calls.
@MikeStay: and does that using a fully featured JS parser (esprima), to produce an AST, which then it walks over github.com/codalien/operator-overloading-js/blob/master/lib/… I think jQuery uses the same parser. I'm not sure if jQuery allows any op overloading, but they could if they wanted to...
5

How about something like this?

class Vector2 {
  /**
   * @param {number} x
   * @param {number} y
  */
  constructor(x, y){
    this.x = x;
    this.y = y;
  }

  /** @param {Vector2} vector */
  "+"(vector){
    return new Vector2(this.x + vector.x, this.y + vector.y);
  }

  /** @param {Vector2} vector */
  "=="(vector){
    return this.x === vector.x && this.y === vector.y;
  }

  get "++"(){
    return new Vector2(this.x++, this.y++);
  }

  /** @param {Vector2} vector */
  static "++"(vector){
    return new Vector2(++vector.x, ++vector.y);
  }

  /** @param {Vector2} vector */
  set ""(vector){
    this.x = vector.x;
    this.y = vector.y;
    return this;
  }

};

const vec1 = new Vector2(10, 20);

// get "++"(){}
vec1["++"];

const vec2 = new Vector2(20, 30);
// static "++"(vec1){}
Vector2 ["++"](vec1);

// set ""(vec2){}
vec1[""] = vec2;

const isEqual = (vec1) ["=="] (vec2);

const vec3 = new Vector2(1, 2);
const sumVec = (vec1) ["+"] (vec2) ["+"] (vec3);

3 Comments

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
I love it! This is a wonderful abuse of notation.
Funnily enough, the Graal VM which does 'true' op overloading as an experimental option, lets you define the ops with almost the same syntax (not entirely though, the syntax used there is more like a friend function in other languages, rather than a member op), although accessing/using the overloaded ops is much more straightforward thereafter. graalvm.org/latest/reference-manual/js/OperatorOverloading
0

Self plug: I wrote a library to do this because TC39 recently abandoned the proposal to introduce it: https://www.npmjs.com/package/@nano-utils/op

User-side looks like:

import { op } from '@nano-utils/op';

class Vector {
    constructor(...elems) {
        this.arr = elems;
    }

    'operator+'(other) {
        return new Vector(...this.arr.map((e, i) => e + other.arr[i]));
    }
}

const a = new Vector(1, 2),
    b = new Vector(3, 4);

console.log(op`${a} + ${b}`); // -> Vector(4, 6)

Comments

0

One approach could be to translate code from the inline operator syntax to method syntax. For this you could use a function that runs once on the functions that you want to have this behaviour.

For the case in your question, you would run it as follows:

const main = Overload.enable(function () {
    var x = new Vector2(10, 10);
    var y = new Vector2(10, 10);
    
    x += y;
    
    console.log(x);
});

main();

Here is how Overload could be defined -- it would be a library to include in your project:

const Overload = {
    // Default for how operators should work:
    binaryOperators: {
        "+"(a, b) { return a + b },
        "-"(a, b) { return a - b },
        "*"(a, b) { return a * b },
        "/"(a, b) { return a / b },
        "%"(a, b) { return a % b },
    },
    unaryOperators: {
        "-"(a) { return -a },
    },
    // Functions that perform operations, being aware of potential overloads
    binary(operator, left, right) {
        return Object(left)[operator] ? Object(left)[operator](right)
             : Object(right)[operator] ? Object(right)[operator](left, true)
             : operators[operator](left, right);
    },
    unary(operator, operand) {
        return Object(operand)[operator] ? Object(operand)[operator]() 
             : operators[operator](operand);
    },
    // Create a new version of a given function 
    // replacing the operators in its source code with calls of the above functions.
    // This has a dependency on the esprima parser
    enable(f) {
        const code = "(" + f + ")";
        const splices = [];
        esprima.parseScript(code, {range:true}, function(node) {
            if (node.type === "BinaryExpression") {
                splices.push([node.range[0], 0, `Overload.binary("${node.operator}",`]);
                splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, ","]);
            } else if (node.type === "AssignmentExpression") {
                splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, 
                        `= Overload.binary("${node.operator.slice(0, -1)}",${code.slice(...node.left.range)},`]);
            } else if (node.type === "UnaryExpression") {
                splices.push([node.range[0], node.operator.length, `Overload.unary("${node.operator}",`]);
            } else return;
            splices.push([node.range[1], 0, ")"]);
        });
        // Apply all collected code changes:
        const arr = [...code];
        for (const [a, b, c] of splices.sort(([a, c], [b, d]) => b - a || d - c)) {
            arr.splice(a, b, c); 
        }
        // Perform an indirect(!!) eval, so that it runs in the global scope (not local)
        // If the input function is a named function, the name is reused for the new function
        return eval?.(arr.join("")); 
    }
};

Here is all of that combined:

const Overload = {binaryOperators: {"+"(a, b) { return a + b },"-"(a, b) { return a - b },"*"(a, b) { return a * b },"/"(a, b) { return a / b },"%"(a, b) { return a % b },},unaryOperators: {"-"(a) { return -a }},binary(operator, left, right) {return Object(left)[operator] ? Object(left)[operator](right): Object(right)[operator] ? Object(right)[operator](left, true): operators[operator](left, right);},unary(operator, operand) {return Object(operand)[operator] ? Object(operand)[operator]() : operators[operator](operand);},enable(f) {const code = "(" + f + ")";const splices = [];esprima.parseScript(code, {range:true}, function(node) {if (node.type === "BinaryExpression") {splices.push([node.range[0], 0, `Overload.binary("${node.operator}",`]);splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, ","]);} else if (node.type === "AssignmentExpression") {splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, `= Overload.binary("${node.operator.slice(0, -1)}",${code.slice(...node.left.range)},`]);} else if (node.type === "UnaryExpression") {splices.push([node.range[0], node.operator.length, `Overload.unary("${node.operator}",`]);} else return;splices.push([node.range[1], 0, ")"]);});const arr = [...code];for (const [a, b, c] of splices.sort(([a, c], [b, d]) => b - a || d - c)) {arr.splice(a, b, c);}return eval?.(arr.join(""));}};

// Demo

class Vector2 extends Array {
    constructor(x, y) {
        super(x, y);
    }
    // The following prototype method intends to overload operator functionality:
    ["+"](other) {
        return new Vector2(this[0] + other[0], this[1] + other[1]);
    }
}

const example = Overload.enable(function () {
    var x = new Vector2(10,10);
    var y = new Vector2(10,10);

    x += y;
    
    console.log(x);
});

example();
<script src="https://unpkg.com/esprima@~4.0/dist/esprima.js"></script>

Here is an example with an implementation of complex numbers:

const Overload = {binaryOperators: {"+"(a, b) { return a + b },"-"(a, b) { return a - b },"*"(a, b) { return a * b },"/"(a, b) { return a / b },"%"(a, b) { return a % b },},unaryOperators: {"-"(a) { return -a }},binary(operator, left, right) {return Object(left)[operator] ? Object(left)[operator](right): Object(right)[operator] ? Object(right)[operator](left, true): operators[operator](left, right);},unary(operator, operand) {return Object(operand)[operator] ? Object(operand)[operator]() : operators[operator](operand);},enable(f) {const code = "(" + f + ")";const splices = [];esprima.parseScript(code, {range:true}, function(node) {if (node.type === "BinaryExpression") {splices.push([node.range[0], 0, `Overload.binary("${node.operator}",`]);splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, ","]);} else if (node.type === "AssignmentExpression") {splices.push([code.indexOf(node.operator, node.left.range[1]), node.operator.length, `= Overload.binary("${node.operator.slice(0, -1)}",${code.slice(...node.left.range)},`]);} else if (node.type === "UnaryExpression") {splices.push([node.range[0], node.operator.length, `Overload.unary("${node.operator}",`]);} else return;splices.push([node.range[1], 0, ")"]);});const arr = [...code];for (const [a, b, c] of splices.sort(([a, c], [b, d]) => b - a || d - c)) {arr.splice(a, b, c);}return eval?.(arr.join(""));}};

class Complex {
    constructor(real, img) {
        this.real = real;
        this.img = img;
    }
    toString() {
        if (!this.img) return `${this.real}`;
        if (!this.real) return `${this.img}i`;
        return `${this.real}${this.img < 0 ? "" : "+"}${this.img}i`;
    }
    static cast(other) {
        if (other instanceof Complex || other == null) return other; // Pass-thru
        if (Object(other) !== other) return new Complex(Number(other), 0); // Primitive
        if (other.length === 2) return new Complex(other[0], other[1]); // Array-like
        throw "Cannot cast this to Complex";
    }
    // The following prototype methods will override operator functionality:
    ["+"](other) {
        other = Complex.cast(other);
        return new Complex(this.real + other.real, this.img + other.img);
    }
    ["-"](other, reversedOperands=false) {
        // Unary minus?
        if (arguments.length === 0) return new Complex(-this.real, -this.img);
        // Binary minus:
        other = Complex.cast(other);
        return reversedOperands ? this["-"]()["+"](other) : other["-"]()["+"](this);
    }
    ["*"](other) {
        other = Complex.cast(other);
        return new Complex(this.real * other.real - this.img * other.img, this.real * other.img + this.img * other.real);
    }
    ["/"](other, reversedOperands=false) {
        other = Complex.cast(other);
        if (reversedOperands) return other["/"](this);
        const denom = other.real * other.real + other.img * other.img;
        return new Complex((this.real * other.real + this.img * other.img) / denom,
                           (this.img * other.real - this.real * other.img) / denom);
    }
}

// Decorate the function(s) that need operator overrides:
const example = Overload.enable(function (a, b) {
    return 8 * (a - b) / (a + b);
});

const result = example(new Complex(10, 6), new Complex(4, 8))
console.log("result: "  + result);
<script src="https://unpkg.com/esprima@~4.0/dist/esprima.js"></script>

Remarks:

This approach has no limitation on the number of operations that are performed in one expression.

For assignment operators, such as +=, this solution silently assumes that there are no side-effects being performed in the left-hand side of the assignment. If this is the case, the converted code would execute that side effect twice, like in this example:

a[i++] += a[j];

This would be executed as:

a[i++] = Overload.binary("+", a[i++], a[j]);

...and so you see i gets incremented twice. To avoid this, make sure that the left-hand side has no side effects. If the accessed property of a is not a getter, then instead do:

a[i] += a[j];
i++;

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.