Javascript member values can bleed into other instances
I recently got bit by a surprising reality of Javascript prototype objects: changes to one instance of an object can directly affect the values held by another instance. An example:
function Person() {}
Person.prototype.name = '';
Person.prototype.phones = [];
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.setName = function (value) {
this.name = value;
};
Person.prototype.getPhones = function () {
return this.phones;
};
Person.prototype.addPhone = function (number) {
this.phones.push(number);
};
Then, instantiate two instances, but only set values for the first:
var mary = new Person();
mary.setName('Mary');
mary.addPhone('123-456-7890');
var tom = new Person();
console.log('Name: ' + tom.getName()); // shows blank name (good)
console.log('Phone: ' + tom.getPhones()[0]); // shows 123-456-7890 (BAD!)
The problem is that all instances share the same prototype. Not just the same prototype definition as I had previously thought, but the actual prototype object. So primitives (name, in the above example) work fine, but not for member object variables (phones), where the prototype holds an object reference.
A safe and clean solution is to not do member variable declarations outside of the constructor. So rather than declaring like:
Person.prototype.phones = [];
Instead, put them in the constructor only:
function Person() {
this.phones = [];
}
This assures that each instance gets a "fresh" member object, rather than a recycled one.
Here's a JSFiddle illustrating the problem.
Thanks goes out to this StackOverflow article for helping me understand the underlying cause of this.
Written by Mike Hedman
Related protips
3 Responses
The thing that one has to remember is that one should not add any non-primitive properties directly to the prototype, in otherwords, if typeof object == 'object'
, add it as a member in the constructor. Functions are OK to add directly to the prototype as well, unless you are doing something crazy like using it has some sort of value store.
Here is a list of things that you can add directly to the prototype:
- boolean
- number
- string
- function
- null
- undefined
strings, numbers, and booleans are always coppied instead of being passed by reference (unless they are the non-primitive variants, i.e. created with new String
, etc.). I will also add objects to the prototype if it is a sort of default value, but never anything that will be storing instance specific data, instead add that in the constructor.
// never
function Ctor() {}
Ctor.prototype.store = [];
// always
function Ctor() {
this.store = [];
}
@Kaleb - Good points.
I would still be wary of adding nulls and undefined's - presumably they will not be uninitialized forever. That's why I suggest abandoning member declaration (other than functions) outside of the constructor. If there are NO members declared outside of the constructor, then it's sure that you are OK.
@mike hedman - I usually will add a null so that all public properties are in one place. It aids in documentation.
/**
* @constructor
* @param {string} name - given name of this `Person`
*/
function Person(name) {
// here we override a default value if an alternative value is provided
if(name) {
this.name = name;
}
// The array is created inside the constructor so as to not have every Person instance share the same array
this.property = [];
}
Person.prototype = {
constructor: Person,
// All of my properties are now going to be documented in the same location
/**
* The given name of the `Person`
* @type {string}
*/
name: 'Anonymous',
/**
* List of all the objects owned by this `Person`
* @type Array<*>
*/
property: null
}