3

I have an object that sits on the prototype, if it has a simple property like bar:3, then any instance can change bar without affecting other instances. However, if it has an object property instead (baz), any instance changing bar.x will be reflected on all other instances, I presume because objects are by reference.

Example object that sits on a prototype:

var foo = {
  bar: 3, // single prop - this is okay
  baz:{x: 4,y:5} // object prop - an instance can't change baz.x
};

My question - in the code below, how do I make porsche show "object prop: V12"?

jsfiddle

var vehicle = {
  colour: 'blue',
  info: function() {
    console.log('wheels:' + this.wheels + ' colour:' + this.colour);
  }
};

var engine = {
  size: 'V12', // single prop
  type:{size: 'V12',fuel:'petrol'}, // object prop
  showSize: function() {
    console.log('single prop: ' + this.engine.size );
    console.log('object prop: ' + this.engine.type.size);
  }
};

var car = Object.assign(Object.create(vehicle), {
    wheels: 4,
    drift: function() { console.log('drifting'); }
});

var ferrari = Object.assign(Object.create(car), {
    colour:'red',
    engine: Object.create(engine)
});                   

var porsche = Object.assign(Object.create(car), {
    colour:'silver',
    engine: Object.create(engine)
});  

// ferrari owner changes his engine
ferrari.engine.size = '100cc';
ferrari.engine.type.size = '100cc';

console.log('ferrari:');
ferrari.engine.showSize.call(ferrari); 

console.log('\nporsche:');
porsche.engine.showSize.call(porsche); 

/* 
OUTPUT

ferrari:
single prop: 100cc
object prop: 100cc

porsche:
single prop: V12
object prop: 100cc <------ WRONG, should be V12

*/

EDIT : for anyone that stumbles across this, I'm going to use this pattern; it's more intuitive for me to create constructors and use call(this). The irony is that it's very close to Amit's answer, but I feel that function constructors aren't in the true spirit of prototypal inheritance/delegation.

Having to do this in each 'class' seems clunky:

car.prototype = Object.create(vehicle.prototype ); // <- new way
car.prototype = new vehicle(); // <- old way
car.prototype.constructor = car;

Instead, I know exactly what's going on using this pattern:

var car = Object.create(vehicle, {

    constructor : { value: function (colour, wheels) {
        vehicle.constructor.call(this, colour, wheels);
        return this;
    }}

});

It's six of one, half a dozen of the other ;)

Data
  • 1,337
  • 11
  • 17
  • see also [Prototypal inheritance - Issues with nested objects](http://stackoverflow.com/q/10131052/1048572) – Bergi Aug 18 '15 at 21:31

2 Answers2

4

You will have to give each instance its own object, there's no way around this.

Your code actually shows that you're already familar with the concept: you've given each car its own engine. We can employ the same pattern to give every engine we create its own type:

var engine = {
  size: 'V12',
  showSize: function() {
    console.log('single prop: ' + this.engine.size );
    console.log('object prop: ' + this.engine.type.size);
  }
};
var enginetype = {
  size: 'V12',
  fuel: 'petrol'
};

…

var ferrari = Object.assign(Object.create(car), {
    colour:'red',
    engine: Object.assign(Object.create(engine), {
        type: Object.create(enginetype);
    })
});

var porsche = Object.assign(Object.create(car), {
    colour:'silver',
    engine: Object.assign(Object.create(engine), {
        type: Object.create(enginetype);
    })
});

(but I'm not saying that duplicating the .size on .type.size is a good design, I'll assume it's only an example)

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I like Object.assign(), but I feel it clutters code; what about this version with a constructor function? https://jsfiddle.net/u9ax5xzz/7/ – Data Aug 19 '15 at 01:17
  • p.s. I forgot to say thank you for help. I'm honoured you took the time reply with such a detailed response. – Data Aug 19 '15 at 01:19
  • @Data: Yes, once initialisation becomes too complex you'd use a factory function or constructor (and there are many variations of this). It just wasn't relevant for this answer, so I used the same pattern as the question. Regarding your fiddle, I really dislike creating an `init` function in the `construct` and then deleting it again. Just [put it right on the prototype](https://jsfiddle.net/u9ax5xzz/10/), or [do everything in the constructor](http://jsfiddle.net/u9ax5xzz/11/). – Bergi Aug 19 '15 at 09:10
  • Those are beautiful patterns, thank you. May I ask which pattern you use, i) constructor func & new or ii) Object.create? Do you have a link to a code template? I want to do it right from the get-go and I keep re-factoring trying to find a pattern that floats my boat, 'tis frustrating there's no standardised pattern(s). – Data Aug 19 '15 at 20:42
  • Constructor function with `.prototype` property and using `new` is pretty much the standard pattern you'll see everywhere (and therefore it's also better optimised by engines). ES6 will also provide syntactic sugar with the `class` notation for it. But `Object.create` has it's own merits, it's more "pure", and especially suited in places that don't need initialisation. Go with the one you like better. – Bergi Aug 19 '15 at 21:31
4

First let's untangle what you've done and why you get the result you do...

Object.create(proto) creates an empty object ({}) and sets it prototype to proto. In your case, this method is used to create an empty object for car, with a vehicle prototype, and the 2 Italian speedsters with prototype car. It is also used to create 2 "engines".

You're also wrapping each of the 3 "car" create calls with a call to Object.assign(target, source) that appends new properties to your target object (the newly created object) as instance properties.

So, what happens is that whenever you access (read or write) a property of an object, if that property belongs to the instance, you'll be reading or writing the value of that specific instance. If however the instance doesn't have that property defined, the prototype chain is traversed till the property is found, and then it is used in the context of the relevant prototype. In your case, what this means is that since the car's engines are empty objects with a shared prototype (the engine object you initialized at the top), accessing properties of the engine really goes to that specific, single instance. If you modify it, you modify it for all objects.

Having said all that, you could be doing things a little different... I prefer using proper constructor functions and create objects with the new keyword.

Here's your code refactored:

function vehicle(colour) {
  this.colour = colour || 'blue'; // blue default if nothing else provided
};
vehicle.prototype.info = function() {
    console.log('wheels:' + this.wheels + ' colour:' + this.colour);
};


function engine(size, fuel) {
  this.size =  size || 'V12'; // V12 default if nothing else provided
  this.fuel = fuel || 'petrol'; // petroc default if nothing else provided
};
engine.prototype.showSize = function() {
    console.log('size: ' + this.size );
};

function car(colour) {
  vehicle.call(this, colour);
  this.wheels = 4;
  this.engine = new engine();
};
car.prototype = new vehicle();
car.prototype.constructor = car; // <-- otherwise, (new car()).constructor != car
car.prototype.drift = function() { console.log('drifting'); };

var ferrari = new car('red');
var porsche = new car('silver');

// ferrari owner changes his engine
ferrari.engine.size = '100cc';

console.log('ferrari:');
ferrari.engine.showSize();
ferrari.info();

console.log('\nporsche:');
porsche.engine.showSize();
porsche.info();
Amit
  • 45,440
  • 9
  • 78
  • 110
  • Porsche is actually not an Italian speedster, but still a good answer. +1 – DavidDomain Aug 18 '15 at 21:51
  • @Amit - Thank you for your answer, much appreciated. I'm not too keen on using new, it just doesn't float my boat. I don't know why, perhaps it's the parent call and then reassigning constructors. It doesn't feel very object orientated (in a prototypal sense). – Data Aug 19 '15 at 01:21
  • @Data - that's OK. There's more than one way to do things, and you'll usually have a preference... Regarding usage of `new`, well I guess it won't be long before you'll start playing/using es6 classes... When you do, you'll use `new`, and the pattern I used is much closer to how classes work... – Amit Aug 19 '15 at 03:02