object in prototype is inherited as reference

2019-02-25 00:36发布

问题:

I want to inherit new object instance using prototype.

Test case:

var MyObj = function() {}
MyObj.prototype.objName = {} 
// I want this to be a different object for each instance of MyObj

var o1 = new MyObj (),
    o2 = new MyObj ();

o1.objName['a'] = 1;
o2.objName['a'] = 2;

alert(o1.objName['a']) // 2
alert(o1.objName === o2.objName) // true

This means that objects in prototype are not inherited as its copies but instead as its reference.

I know that normally you can do it like this.

var MyObj = function() {
    this.objName = {}
}

var o1 = new MyObj(),
    o2 = new MyObj();

alert(o1.objName === o2.objName) // false

This works fine, but in my case this is not an option. I really need to define objName outside the MyObj function.

I managed to "solve" the problem with this

MyObj.prototype.objName = function() {
    if ( this._objName === undefined ) {
        this._objName = {};
    }
    return this._objName;
}

var o1 = new MyObj(),
    o2 = new MyObj();

o1.objName()['a'] = 1;
o2.objName()['a'] = 2;

alert(o1.objName()['a']) // 1

But this is not very pretty and the performance of this code is much worse.

Is there any way to solve this more elegantly ?

回答1:

This means that objects in prototype are not inherited as its copies but instead as its reference.

Nothing on the prototype is copied - the whole concept of prototypical inheritance is that properties reference the shared properties of the prototype object. So if you want a property to be individual for each instance, you have to explicitly assign it to the object and shadow the prototype property; just as you're doing it with the _objName property in your code.

But this is not very pretty and the performance of this code is much worse.

If you want it pretty, move it to the constructor (or make the constructor look for something like an init method to call if exists, then you can create that init method on the prototype.

To make performance a little better, you can change the getter function to

MyObj.prototype.getObj = function() {
    var obj = {};
    this.getObj = function(){ return obj; }; // overwrite itself
    return obj;
};

though it still has the function call overhead. For even more elegance, you can use a getter property (not supported in old browsers) that removes itself on the first access:

Object.defineProperty(MyObj.prototype, "objName", {
    get: function() {
        var obj = {};
        Object.defineProperty(this, "objName", {
            value: obj,
            writable: true //?
        });
        return obj;
    },
    enumerable: true,
    configurable: true
});

Now you can omit the function call parenthesis.



回答2:

This means that objects in prototype are not inherited as its copies but instead as its reference.

Just to be clear. First of all in JavaScript all objects are passed by reference, not by value. Only primitives are passed by value. Second, you're not actually "copying" or "passing" anything. When you set a prototype, you're creating a prototype's chain. It means that in your case:

var MyObj = function() {};
MyObj.prototype.objName = {} ;

var o1 = new MyObj ();
var o2 = new MyObj ();

Both o1 and o2 doesn't have any property called objName, and you can simply test it with:

console.log(Object.keys(o1)); // []

When JS see a code like o1.objName, as first thing checks if the object has this property, and if it has, use it. If not, start to looking in the prototype's chain, starting by the prototype of o1, that is MyObj.prototype: it found the properties objName, and returns it. If it didn't find it, then JS will continue to check the prototype of MyObj.prototype, and so on. So, here the point: MyObj.prototype it's an object: and you shared that object between o1 and o2. That's why the instance of objName is the same. It's exactly the same logic of having:

function objName(obj) {
    return "objName" in obj ? obj.objName : O.objName;
}

var O = { objName: [] };
var foo = {};
var bar = {};

objName(foo).push(0);
objName(bar).push(1);

So, you can't put in prototype any object that is not meant to be shared across the objects creates using that prototype. I would say that shared states like that is also a bad practice that should be avoided, that's why in general prototype shouldn't have such property. It's still not clear to me why you can't modify the constructor, but the point is: when you create the instance of your object, you have to "setup" it. Usually, calling the constructor, but any function is fine. This is made also when you want to support inheritance, and calling the "super" constructor to initialize your object.