Indiscripts

  • Products
  • Snippets
  • Tips
  • Extras
  • ?
  • Wordalizer
  • IndyFont
  • StyLighter
  • FontMixer
  • HurryCover
  • BookBarcode
  • IndexMatic
  • Equalizer
  • InGutter
  • YALT
  • Claquos
Right-click and Download ComplexClass.js
Version 1.0 EN (05/25/10) for InDesign CS2/CS3/CS4

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


gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.