3

In my JavaScript project I define an object, then create multiple instances using Object.create(). The object has several (string and int) properties, each of which is unique per instance. However, if I use an array property, all instances share the same array.

This code easily demonstrates this:

TestThing = {
    code: "?",
    intlist: [],

    addint(i) {
        alert("Adding " + i + " to " + this.code + ", list had " + this.intlist.length + " ints");
        this.intlist.push(i);
    }
}

var thing1 = Object.create(TestThing);
thing1.code = "Thing 1";
var thing2 = Object.create(TestThing);
thing2.code = "Thing 2";

thing1.addint(11);
thing2.addint(42);

alert(thing2.intlist);  // will output 11,42

So, what causes this? How do I solve this issue?

ErikvdW
  • 113
  • 1
  • 6
  • 1
    *"The object has several (string and int) properties, each of which is unique per instance."* - no, it's just that you treat them in a different way. `thing1.code = "Thing 1";` is fundamentally different to `thing1.intlist.push(11);`. One's modifying, one's replacing. – jonrsharpe Nov 02 '17 at 18:11
  • 1
    The inner array is not a copy..it is a reference to original array – charlietfl Nov 02 '17 at 18:12
  • So how do I make sure each instance has it's own array? edit: I mean I know how to replace the array with a new ne per instance, but that's not very elegant. What would be best practice in this case? – ErikvdW Nov 02 '17 at 18:15

2 Answers2

1

With reference-type properties, each child gets a reference to the same object. Any change any child makes to the object are visible to all instances.

You need to either implement a constructor to set up the property, or have the code that uses the property set it up the first time through. (If you want to use a constructor and Object.create, though, you'll have to call it yourself; Object.create won't call it for you.)

You could do something like this...

TestThing = {
    code: "?",
    intlist: null,
    addint : (i) => {
        if (!this.intlist) this.intlist = [];
        alert("Adding " + i + " to " + this.code + ", list had " + this.intlist.length + " ints");
        this.intlist.push(i);
    }
}

Or, less error-prone-ly (albeit forsaking Object.create)...

class TestThing {
    constructor(code) {
        this.code = code;
        this.intlist = [];
    }

    addint(i) {
        alert("Adding " + i + " to " + this.code + ", list had " + this.intlist.length + " ints");
        this.intlist.push(i);
    }
}

var thing1 = new TestThing("Thing 1");
var thing2 = new TestThing("Thing 2");

thing1.addint(11);
thing2.addint(42);

alert(thing2.intlist);  // will output 42

Unfortunately, if you're coding for web browsers, IE (even IE 11) doesn't seem to support class. So you'll have to stick with the old way of defining classes.

TestThing = function(code) {
    this.code = code;
    this.intlist = [];
};

TestThing.prototype = {
    addint: function(i) {
        alert("Adding " + i + " to " + this.code + ", list had " + this.intlist.length + " ints");
        this.intlist.push(i);
    }
};
cHao
  • 84,970
  • 20
  • 145
  • 172
  • I can see how this works around the issue, but this solution becomes almost unworkable if I have a lot of functions (or even other objects) accessing the property. I was hoping for a way to create the inheriting objects without running into this problem. – ErikvdW Nov 02 '17 at 18:28
  • That'd be a job for a constructor. Unfortunately, if you use `Object.create`, it won't call the constructor for you. – cHao Nov 02 '17 at 18:31
  • @cHao however the second parameter may help: `const instance = Object.create(parent, {list: [] });` – Jonas Wilms Nov 02 '17 at 18:43
  • Thanks @cHao, I'll think I'll switch to the more elegant way in your second example. – ErikvdW Nov 02 '17 at 18:45
  • @Jonasw: That second argument to `Object.create` isn't quite as cool as it first looks. (Its values are supposed to be property descriptors.) Plus, it's still error-prone, though. You could, if you wanted, say `Object.new = function(parent, ...args) { let retval = Object.create(parent); retval.constructor(...args); return retval; }`, if you wanted. – cHao Nov 02 '17 at 19:02
  • But, that `Object.new` doesn't add any support for inheritance more than one level deep. – cHao Nov 02 '17 at 19:08
-2

To fix this you use concat instead of push, like so:

this.intlist = this.intlist.concat(i);

Why this happens ? Because push mutates the array, concat doesn't, and an array in javascript is also an object, hence, the memory reference to that array is the same.

João Vilaça
  • 601
  • 1
  • 6
  • 13
  • 1
    I feel this is a dangerous way to do this - all instances will still share the same array until they add something to it. I want to use the array in several ways, not just this one function. Your solution seems error-prone, and inefficient if I use the add function a lot (say, 10,000's of times). – ErikvdW Nov 02 '17 at 18:25
  • you can also use Object.assign({}, TestThing) or use spread operator to add to the list. – João Vilaça Nov 02 '17 at 18:27
  • But the problem is not adding things to the list (the code is just an example). The problem is that I may have several functions and external objects and functions accessing the property, and using it in several ways. For this, all instances should have their own instance of the array. – ErikvdW Nov 02 '17 at 18:31