Do Arrays and Objects have side effects?

In my last post about static initializers I talked about member initializations with side effects, which need to be neutralized. This was one of the examples for “safe” initializers without any side effects:

// ActionScript:
public class Foo
{
    public const m_instance_member : String = "Instance Member";
}
// JavaScript:
var Foo = function() {};
Foo.prototype.m_instance_member = "Instance Member";

The example above used a String on purpose. As it turns out Arrays and Objects are not always safe and may introduce side effects in situations you might not expect.

 

Ninjas are Persons too

I have a simple question for you: Are Arrays safe, too?

// ActionScript:
public class Person
{
    public var m_array : Array = [];
}

Looks pretty safe, doesn’t it? Shouldn’t we transform that snippet as follows?

// JavaScript:
var Person = function() {};
Person.prototype.m_array = [];

That also looks okay. But problems appear when you use Person as a base class for another class.

// ActionScript:
public class Ninja extends Person
{
}

 

// JavaScript:
var Ninja = function() {};
goog.inherits( Ninja, Person );

What can possibly be wrong with this transformation? Well, try this code:

// ActionScript:
var person : Person = new Person();
var ninja : Ninja = new Ninja();
person.m_array.push(123);
trace( ninja.m_array.length );

 

// JavaScript:
var person = new Person();
var ninja = new Ninja();
person.m_array.push(123);
trace( ninja.m_array.length );

We create an instance of Person and an instance of Ninja. Then we add the number 123 to person‘s m_array and print out the length of ninja‘s m_array. The expected result should be of course 0. So, what’s the problem? Unfortunately, if we don’t treat Array as an initializer with side effects the generated code will yield 1 instead of 0. For some odd reason adding 123 to person‘s  m_array somehow affects ninja‘s m_array as if m_array were shared between the two classes.

 

Closure-library’s Issue 396

Last month I wrote up this bug against Google’s Closure Library, which was filed as issue 396:

Issue 396: goog.inherits() does not clone arrays and objects
What steps will reproduce the problem?

1. Run this code, which uses goog.inherits
var Person = function() {};
Person.prototype.m_array = [];
var Ninja = function() {};
goog.inherits( Ninja, Person );
var p = new Person();
var n = new Ninja();
p.m_array.push(123);
console.info( n.m_array ); 

2. Observe result in the debug console
Result is: "[123]"

What is the expected output? What do you see instead?
"[]"
The code in #1 is pretty much identical with the code I introduced above. So why is the result “[123]” and not “[]”? Here is what the friendly folks from Google told me:

Nicholas J. Santos: right, this is working as intended. All objects on the prototype are shared. I believe Closure Linter will warn you about this if you do it.

Jay Young: It sounds like you expect goog.inherits to copy members from one class to another (also known as mixins or composition-based inheritance). That is not the case. Closure uses true prototypal inheritance. Anything on a superclass prototype is shared with all subclasses as well. Closure style has a rule that only primitive values should be placed on the prototype chain. Most often, the intention is to use an empty object/array as a default, but what ends up happening is exactly what you described: methods end up mutating the prototype object instead of an instance-specific one.

I hope Nicholas and Jay will forgive me, because I submitted this problem as a bug even though I had already known that this wasn’t a bug. I just wanted to get an official explanation that I could share with you. It might not be worth diving into every detail. But the problem lies in the way goog.inherits emulates classes by merging prototype chains. I suspect that every class emulating JavaScript library has that problem. I know that John Resig’s simple JavaScript inheritance code does.
But how do we fix this problem?

 

Neutralizing Arrays

You might have already guessed the answer. Even though Arrays and Objects don’t have any side effects per se we should still treat them as initializers with side effects, because Arrays and Objects get shared when using prototypes for emulating inheritance in JavaScript. Fortunately we know how to neutralize side effects.
Here is the complete example:
// ActionScript:
public class Person
{
    public var m_array : Array = [];
}
public class Ninja extends Person
{
}

 

var person : Person = new Person();
var ninja : Ninja = new Ninja();
person.m_array.push(123);
trace( ninja.m_array.length ); // Expected: 0 and not 1

And here is the transformed JavaScript:

// JavaScript:
var Person = function()
{
    var self = this;
 self.m_array = [];
};
Person.prototype.m_array = null;

 

var Ninja = function() {};
goog.inherits( Ninja, Person );

 

var person = new Person();
var ninja = new Ninja();
person.m_array.push(123);
trace( ninja.m_array.length ); // Expected: 0 and not 1

 

Conclusion

Arrays and Objects introduce side effects when used as static initializers in base classes, because most JavaScript libraries chain prototypes for emulating inheritance. For that reason I recommend adding Arrays and Objects to the list of expressions with side effects when used as static initializers, which need to be neutralized.