This post describes how to do a limited version of operator overloading in JavaScript. With the technique described here, you’ll be able to implement a type StringBuilder that can be used as follows:
var sb = new StringBuilder(); sb << add("abc") << add("def");And a type Point that can be used as follows:
var p = new Point(); p._ = new Point(1, 2) + new Point(3, 4) + new Point(5, 6); p._ = new Point(1, 2) * new Point(3, 4) * new Point(5, 6);
obj1 + obj2The plus operator is applied to two objects. This triggers the method call
obj1.operator+(obj2)The closest thing you can achieve in JavaScript is fake operator overloading – triggering two method calls:
obj1.valueOf() obj2.valueOf()Those are made because + only works with primitive values and thus needs to convert obj1 and obj2 to primitives. It does so by invoking their valueOf() method. Fake operator overloading is much less useful than real operator overloading: You don’t get access to both operands at the same time and you can’t influence the value returned by +. We’ll later look at tricks that work around these limitations.
What operators can be used for fake operator overloading? All operators that coerce (convert) their operands to primitives. The following two objects allow you to test which ones do:
var obj1 = { valueOf: function () { console.log("valueOf1"); return 1; } }; var obj2 = { valueOf: function () { console.log("valueOf2"); return 2; } };For example:
> obj1 + obj2 valueOf1 valueOf2 3The following binary operators coerce:
+ - * / % & | ^ << >> >>> < <= > >=Equality operators, inequality operators, and boolean operators can work with objects and thus don't coerce. For example:
> obj1 === obj2 false > obj1 && obj2 { valueOf: [Function] }Triggering calls. If its operands are produced by function calls, then a binary operator triggers a total of four function (or method) calls: First, it evaluates the two operand expressions to values (in this case, objects). Then, it converts two objects to primitives. The following function allows us to examine what happens and in which order:
function func(x) { console.log("func_"+x); return { valueOf: function() { console.log("valueOf_"+x) } }; }Let’s use it to examine the << operator:
> func("LEFT") << func("RIGHT") func_LEFT func_RIGHT valueOf_LEFT valueOf_RIGHTHence: First, the left operand is evaluated, then the right operand. Next, the left value is converted to a primitive, then the right value. It is slightly perplexing to see that valueOf_LEFT does not happen directly after func_LEFT, but it makes sense if you think of function calls, where one also first evaluates the parameters before executing the function body. Some operators even convert their right operands first:
> func("LEFT") > func("RIGHT") func_LEFT func_RIGHT valueOf_RIGHT valueOf_LEFT
var sb = new StringBuilder(); sb << add("abc") << add("def"); console.log(sb.toString()); // abcdefFake operator overloading allows us to do this:
function StringBuilder() { this.data = ""; } // Called by << StringBuilder.prototype.valueOf = function () { StringBuilder.current = this; }; // Used to access the aggregated string StringBuilder.prototype.toString = function () { return this.data; }; function add(value) { return { valueOf: function () { StringBuilder.current.data += value; } } }Explanation: The << operator calls StringBuilder.prototype.valueOf which marks the current instance as the “receiver” of subsequent “messages” (by storing it in StringBuilder.current). The messages are sent via add(), which wraps its argument value in an object. When that object is contacted by the operator, it adds value to the current receiver.
def ("Person") ({ init: function(name){ this.name = name; }, speak: function(text){ alert(text || "Hi, my name is " + this.name); } }); def ("Ninja") << Person ({ init: function(name){ this._super(); }, kick: function(){ this.speak("I kick u!"); } }); var ninjy = new Ninja("JDD"); ninjy.speak(); ninjy.kick();There are two ways to use the API:
(I) def ("Person") ({...}) (II) def ("Ninja") << Person ({...})Therefore, whatever is returned by def() must be both callable (i.e., a function) and a potential fake operand. Furthermore, there are two ways of invoking Person (which has been created by def.js):
func("LEFT") << func("RIGHT")Can we rewrite func() to get even more calls? Yes, we can:
function func(x) { console.log("func_" + x); return { valueOf: function () { console.log("valueOf_"+x); return {}; // not a primitive }, toString: function () { console.log("toString_"+x); return 0; // a primitive }, } }The above implementation of func() gives us six calls:
> func("LEFT") << func("RIGHT") func_LEFT func_RIGHT valueOf_LEFT toString_LEFT valueOf_RIGHT toString_RIGHTThe trick here is that << internally performs a ToNumber() [1] conversion. ToNumber() first tries valueOf(). If that method does not return a primitive value, it continues with toString(). If toString() does not return a primitive, either, then a TypeError is thrown. Note that ToNumber() only expects the result of either valueOf() or toString() to be a primitive which it then converts to a number. Hence, toString() returning a string is only enforced by convention in JavaScript.
var p = new Point(); p._ = new Point(1, 2) + new Point(3, 4) + new Point(5, 6); console.log(p.toString()); // Point(9, 12) p._ = new Point(1, 2) * new Point(3, 4) * new Point(5, 6); console.log(p.toString()); // Point(15, 48)This works as follows. Each of the operands of the plus operator is converted via valueOf():
Point.prototype.valueOf = function () { Point.operands.push(this); return 3; }This method stores the operand we actually want to use away in a global variable. It then returns 3 (a value that the plus operator can work with). That number is the lowest natural number x for which all of the following expressions produce different results:
x + x x - x x * x x / xp._ has a setter that receives the result of 3 being added, multiplied etc., figures out which operator was used and processes Point.operands accordingly.
Object.defineProperty(Point.prototype, "_", { set: function (value) { var ops = Point.operands; var operator; if (ops.length === 2 && value === 0) { // 3 - 3 operator = this.setSubtract; } else if (ops.length === 2 && value === 1) { // 3 / 3 operator = this.setDivide; } else if (ops.length >= 2 && (value === 3 * ops.length)) { // 3 + 3 + 3 + ... operator = this.setAdd; } else if (ops.length >= 2 && (value === Math.pow(3, ops.length))) { // 3 * 3 * 3 * ... operator = this.setMultiply; } else { throw new Error("Unsupported operation (code "+value+")"); } Point.operands = []; // reset return operator.apply(this, ops); } });For addition and multiplication, the number ops.length of operands can be greater than 2. We therefore test the result value of the operator application as follows: