## Operator Overloading with ExtendScript

May 25, 2010 | Tips | en

Operator overloading in JavaScript is a controversial issue. Actually, this dangerous feature has been rejected in ECMAScript 4. However, ExtendScript allows you to override the behavior of many mathematical and logical operators on a class-by-class basis since CS2.

As a general rule, operator overloading should not be considered as a good programming practice. This feature is overlooked by the JS ExtendScript developers because the situations in which they really benefit from redefining an operator are infrequent and unstable. In most cases, it is far better to explicitly create and invoke the underlying operations through normal object methods.

An operator is nothing but a method. For example, the **Addition operator** internally corresponds to a `prototype['+']`

method which is defined in many native JavaScript classes. `Number.prototype['+']`

implements the regular addition, `String.prototype['+']`

implements the string concatenation, etc. The process of defining a `'+'`

behavior in a new class is similar to creating a `prototype.add`

method:

MyClass.prototype.add = function(/*MyClass*/ obj) { var ret = new MyClass(); // // Here you implement ret = this.add(obj) // return ret; } // Usage: var a = new MyClass(/*a_parameters*/); var b = new MyClass(/*b_parameters*/); var c = a.add(b); // etc.

Now, to plug a `+`

operator in MyClass, we just need to replace the method name (`'add'`

) by the operator name (`'+'`

):

MyClass.prototype['+'] = function(/*MyClass*/ obj) { var ret = new MyClass(); // // Here you implement ret = this + obj // (this is the left operand, obj is the right operand) // return ret; } // Usage: var a = new MyClass(/*a_parameters*/); var b = new MyClass(/*b_parameters*/); var c = a + b; // means: c = a['+'](b) // etc.

In the above code, `a + b`

is interpreted by ExtendScript as `a['+'](b)`

, so this syntactically shortens the method call. The `+`

operator is then available to any MyClass object instance.

ExtendScript allows you to extend —or override!— a number of operators:

Operator | Expression | Default Semantics (ECMA) |
---|---|---|

Unary + |
+X | Converts X to Number. |

Unary – |
–X | Converts X to Number and then negates it. |

Addition |
X + Y | Performs string concatenation or numeric addition. |

Subtraction |
X – Y | Returns the difference of the numeric operands. Equiv: X + (–Y). |

Multiplication |
X * Y | Returns the product of the numeric operands. |

Division |
X / Y | Returns the quotient of the numeric operands. |

Modulus |
X % Y | Returns the floating-point remainder of the numeric operands from an implied division. |

Left Shift |
X << N | Performs a bitwise left shift operation on X by the amount specified by N. |

Signed Right Shift |
X >> N | Performs a sign-filling bitwise right shift operation on X by the amount specified by N. |

Unsigned Right Shift |
X >>> N | Performs a zero-filling bitwise right shift operation on X by the amount specified by N. |

Equals |
X == Y | Performs an abstract equality comparison and returns a boolean. |

Less-than |
X < Y | Performs an abstract relational less-than algorithm and returns a boolean. |

Less-than-or-equal |
X <= Y | Performs an abstract relational less-than-or-equal algorithm and returns a boolean. |

Strict Equals |
X === Y | Performs a strict equality comparison. |

Bitwise NOT |
~X | Converts X to signed 32-bit integer and performs a bitwise complement. |

Bitwise AND |
X & Y | Performs a signed 32-bit integer bitwise AND. |

Bitwise OR |
X | Y | Performs a signed 32-bit integer bitwise OR. |

Bitwise XOR |
X ^ Y | Performs a signed 32-bit integer bitwise XOR. |

**Note 1.** — You **cannot override** the following JavaScript operators:

• Assignment and compound assignment operators (`=`

, `*=`

, `/=`

, `%=`

, `+=`

, `-=`

, `<<=`

, `>>=`

, `>>>=`

, `&=`

, `^=`

, `|=`

).

• Prefix/Postfix Increment/Decrement operators (`++`

, `--`

).

• Logical operators (`!`

, `&&`

, `||`

), and the conditional operator (` ? : `

).

• Syntactic operators (`new`

, `delete`

, `typeof`

, `void`

, `instanceof`

, `in`

), and the comma operator (` , `

).

**Note 2.** — The `>`

and `>=`

operators can't be overridden directly; instead, they are internally implemented by executing `NOT <`

and `NOT <=`

. Similarly, JavaScript performs the `!=`

and `!==`

operators in terms of `NOT ==`

and `NOT ===`

.

**Note 3.** — “The comparison of Strings uses a simple lexicographic ordering on sequences of code unit values. There is no attempt to use the more complex, semantically oriented definitions of character or string equality and collating order defined in the Unicode specification.” (ECMA-262.)

## Example 1 — Cumulative Arrays

The JavaScript `Array`

class has no math operator prototype, probably because *summing* or *multiplying* arrays is not semantically univocal. Even if one considers numeric arrays only, there are several ways to envision the addition, or the multiplication. For instance `A + B`

could be interpreted as a concatenation, while `A * B`

could be regarded as a matrix product. (Note that since `A + B`

has no default meaning for Array objects, JS will convert this expression into `A.toString() + B.toString()`

—which is not very exciting.)

But suppose that every array in your script describes an ordered set of amounts in the form `[x1, x2, ..., xn]`

. One can define a simple addition routine such as:

`[a1, a2, ..., an] + [b1, b2, ..., bn]`

`= [ (a1+b1), (a2+b2), ..., (an+bn) ]`

.

Providing that your algorithm requires a lot of similar operations, the code could be dramatically simplified by using an `Array.prototype['+'] operator`

as following:

Array.prototype['+'] = function(/*operand*/v) { // If the right operand is not an Array, we return undefined // to let JavaScript behave defaultly if( v.constructor != Array ) return; var i = this.length, j = v.length, // clone this and concat if necessary // the overflowing part of v: r = this.concat(v.slice(i)); if( i > j ) i = j; while( i-- ) r[i] += v[i]; return r; } // Sample code (all operations are commutative) var a = [1,2,3]; alert( a + [4,5,6] ); // [5,7,9] // Our implementation also supports // heterogeneous lengths: alert( a + [4,5] ); // [5,7,3] alert( a + [4,5,6,7] ); // [5,7,9,7] alert( a + [] ); // [1,2,3]

When defining or overriding an operator you must keep in mind these important rules:

• **Unary Operators**. — **Unary +** (`+X`

), **unary –** (`–X`

) and **bitwise NOT** (`~X`

) can be overridden. In this case, the first argument passed to `aClass.prototype[OPERATOR]`

is `undefined`

and the unique operand is `this`

. If you define both **unary +** and **binary +** behavior, make sure you provide an implementation for each situation in the function body.

• **Operator Precedence and Associativity**. — ExtendScript applies the rules defined in the JavaScript Operator Precedence & Associativity Chart. For example, `X + Y * Z`

is always interpreted as `X['+'](Y['*'](Z))`

, and `X * Y * Z`

is always interpreted as `(X['*'](Y))['*'](Z)`

(left-to-right).

• **Commutativity and External Binary Operations**. — JavaScript does not assume that an operator is commutative. `X•Y`

is often different from `Y•X`

(concatenation, division, etc.). In general this is not a problem, because internal binary operations mechanism lets you clearly distinguish the left operand (`this`

) from the right operand (first argument passed to the method). However, **external** binary operations can introduce a more complex procedure: if `objectA.prototype['•'](objectB)`

is not implemented, ExtendScript tries to invoke `objectB.prototype['•'](objectA, true)`

. The second argument (`true`

) is then a boolean flag that indicates to `objectB`

that the operands are provided in reversed order, so that the operation to perform is `A•B`

(and not `B•A`

).

## Example 2 — Scalar Operations on Arrays

Let's consider an example to clarify the last point. Suppose we've defined a *scalar multiplication* in our Array linear algebra:

Array.prototype['*'] = function(/*operand*/k, /*reversed*/rev) { if( k.constructor != Number ) return; // Here we implement an "external" binary op.: // ARRAY * NUMBER var i = this.length, r = this.concat(); // clone this while( i-- ) r[i] *= k; return r; } // Sample code: var a = [1,2,3]; // array * number is directly implemented // (reversed == false) alert( a * 5 ); // [5,10,15] // number * array is indirectly implemented // it's equivalent to array['*'](number, true) // (reversed == true) alert( 5 * a ); // [5,10,15] (works also!)

We are lucky! Since the scalar multiplication is commutative, both `a * 5`

and `5 * a`

are perfectly computed, returning the same result. In the second case (`5 * a`

), ExtendScript internally invokes `a['*'](5,true)`

—provided that there is no support for `Number.prototype['*'](Array)`

.

Now let's implement a *scalar division* using the same approach. There is a critical difference: `a/5`

means something —i. e. `a*(1/5)`

,— but `5/a`

should be rejected until we have not defined Array inversion. So we must use the `reversed`

flag to check the order of the operands, and restrict the operation to the `Array/Number`

syntax.

Array.prototype['*'] = function(/*operand*/k) // Scalar multiplication is OK : Array * Number == Number * Array { if( k.constructor != Number ) return; var i = this.length, r = this.concat(); while( i-- ) r[i] *= k; return r; } Array.prototype['/'] = function(/*operand*/k, /*reversed*/rev) // Scalar division : Array / Number only! { if( k.constructor != Number ) return; if( rev ) return; // Hey! We do not implement Number / Array ! return this*(1/k); // Using already defined Array.prototype['*'] } // Sample code: var a = [10,20,30]; // array / number is implemented alert( a / 10 ); // [1,2,3] // number / array is not implemented alert( 10 / a ); // NaN

**Note**. — When working on `Array`

objects, never forget to clone the data in a new array when you want to return an independent object. Unlike strings, which are automatically cloned by JavaScript when required, arrays are always assigned “by reference”. If you write something like `arr2 = arr1`

, the `arr2`

variable will still reference the actual elements of `arr1`

and not a copy of them. Therefore, modifying `arr2[i]`

will also modify `arr1[i]`

(which is the same “thing.”) An usual way to make a copy of `arr1`

is: `arr2 = arr1.concat()`

, or `arr2 = arr1.slice()`

. You then obtain a **shallow copy**, but this is sufficient for one-dimensional arrays that only contain primitive data.

## Example 3 — A Complex Class

Geometrical scripts use and re-use point transformations (translation, rotation, scaling . . .) that generally can be described as operations in a two-dimensional real vector space. Rather than prototyping Array operators, a possible way to handle [x,y] points in your process is to create a `Complex`

class. Each complex number `z = x + iy`

represents a point in the coordinate system (*complex plane*).

The code below offers various methods and operators that make really easy to compute such operations:

var Complex = (function() { // UTILITIES // --------------------------- var mSqr = Math.sqrt, mCos = Math.cos, mSin = Math.sin, mAtan2 = Math.atan2, kDeg = 180/Math.PI, kRad = Math.PI/180, m2 = function(){return this.x*this.x+this.y*this.y;}, eq = function(z){return this.x==z.x && this.y==z.y;}, cx = function(z){return z instanceof cstr;}, df = function(z){return cx(z)?z:{x:+(z||0),y:0};}; // CONSTRUCTOR // --------------------------- var cstr = function Complex(re,im) { this.x = +re; this.y = +im; }; // INTERFACE (INCLUDING OPERATORS) // --------------------------- cstr.prototype = { toString: function() { return [this.x,this.y].toString(); }, valueOf: function() { return (this.y)?NaN:this.x; }, // MAGNITUDE : |Z| mag: function() { return mSqr(m2.call(this)); }, // INVERSION : 1/Z inv: function() { if( this==0 ) return NaN; return (~this)*(1/(m2.call(this))); }, // ARGUMENT (PHASE) in radians arg: function() { if( this==0 ) return NaN; return mAtan2(this.y,this.x); }, // ARGUMENT (PHASE) in degrees deg: function() { return kDeg*this.arg(); }, // ADDITION "+": function(z) { // Supports: +Z | Z+X | X+Z | Z+Z' var xy = df(z); return new cstr( this.x + xy.x, this.y + xy.y ); }, // MULTIPLICATION "*": function(z) { // Supports: Z*X | X*Z | Z*Z' var xy = df(z); return new cstr( this.x*xy.x - this.y*xy.y, this.x*xy.y + this.y*xy.x ); }, // SUBTRACTION "-": function(z,rev) { // Supports: -Z | Z-X | X-Z | Z-Z' if( !z ) return new cstr(-this.x,-this.y); return (rev)?(z+(-this)):(this+(-z)); }, // DIVISION "/": function(z, rev) { // Supports: Z/X | X/Z | Z/Z' if( cx(z) ) return this*(z.inv()); return (rev)?(z*this.inv()):this*(1/z); }, // EQUALITY "==": function(z) { // Supports: Z==X | X==Z | Z==Z' return eq.call(this,df(z)); }, // CONJUGATE FORM "~": function() { // Supports: ~Z return new cstr(this.x,-this.y); }, // INTEGER POWER "^": function(n,rev) { // Supports: Z^N if( rev || n!==~~n ) return NaN; if( n==0 ) return cstr.unity; if( n<0 ) return 1/(this^(-n)); var r = +this; // clone while( --n ) r*=this; return r; }, // ROTATION "<<": function(/*degrees*/aDeg,rev) { // counterclockwise rotation about [0,0] if( rev ) return NaN; if( !aDeg ) return +this; var a = aDeg*kRad, z = new cstr(mCos(a),mSin(a)); return this*z; }, ">>": function(/*degrees*/aDeg,rev) { // clockwise rotation about [0,0] if( rev ) return NaN; return this<<(-aDeg); }, }; // CONSTANTS // --------------------------- cstr.zero = new cstr(0,0); cstr.unity = new cstr(1,0); cstr.i = new cstr(0,1); return cstr; })(); // Sample code // --------------------------- var z = new Complex(4,3), i = Complex.i; // magnitude and phase var m = z.mag(); alert( m ); // 5 alert( z.deg() ); // about 37° // basic operations alert( 2*z - 4*i ); // [8,2] alert( -(z*~z)/(5*i) ); // [0,5] // comparisons alert( (i^2) == -1 ); // true alert( 0 == Complex.zero ); // true alert( (m*m)/z == ~z ); // true // rotation z <<= 30; alert( z ); // about [2,4.6]

As you can see, once the `Complex`

class is available in your code, you can handle complex numbers as easily and profitably as simple Number objects!

## Comments

Wicked!

Why doesn't this work in regular ECMAScript?

Hi KooiInc,

Operator overloading is ExtendScript-specific, the ECMA-262 standard in itself does not allow this feature.

Why? It's a complicated discussion. See Joe Walker's 2007 post on this topic: “Operator overloading in Javascript 2 and a potential monster CSRF hole” (http://bit.ly/gq5o5b).

See also this post from Nicolas Garcia Belmonte: “Why not Operator Overloading in JavaScript?” (http://bit.ly/62QluF)

@+

Marc