I’m a big fan of Stack Overflow and I tend to contribute regularly (am currently in the top 0.X%). In this category (stackoverflow) of posts, I will be posting my top rated questions and answers. This, btw, is allowed as explained in the meta thread here.
I asked the following question which turned out to be quite a popular one:
From this blog post we have this example of a prototypal inheritance in JavaScript:
var human = {
name: '',
gender: '',
planetOfBirth: 'Earth',
sayGender: function () {
alert(this.name + ' says my gender is ' + this.gender);
},
sayPlanet: function () {
alert(this.name + ' was born on ' + this.planetOfBirth);
}
};
var male = Object.create(human, {
gender: {value: 'Male'}
});
var female = Object.create(human, {
gender: {value: 'Female'}
});
var david = Object.create(male, {
name: {value: 'David'},
planetOfBirth: {value: 'Mars'}
});
var jane = Object.create(female, {
name: {value: 'Jane'}
});
david.sayGender(); // David says my gender is Male
david.sayPlanet(); // David was born on Mars
jane.sayGender(); // Jane says my gender is Female
jane.sayPlanet(); // Jane was born on Earth
Now, what I’m wondering is how does one properly “override”, for example, the sayPlanet
function?
I tried it like this:
jane.sayPlanet = function(){
console.log("something different");
};
and this works.
However, I also tried it like this:
var jane = Object.create(female, {
name: {value: 'Jane'},
sayPlanet: function(){
console.log("something different");
}
});
but I get a type error.
My questions are:
- how can I add the
sayPlanet
function inside the Object.create
?
- is this at all “a good way” or is there a better (best practice) way?
edit: I figured a way how I can add the sayPlanet
inside the Object.create
:
sayPlanet: {
value: function(){
console.log("something different");
}
}
However, a second question remains. Also, I would appreciate if someone can explain it in a bit deeper level if this is “a good way” to use it like this.
edit #2: As Mahavir pointed below, this is an awful example, because as it turns out you can’t (please correct me if I’m wrong) change the name
of jane
once it has been Object.create
d.
edit #3: (man oh man, this is going to get me in a certain facility where people wear white coats). As @WhiteHat pointed below, indeed you can set a name property to be updatable like this:
var jane = Object.create(female, {
name: {
value: 'Jane',
writable: true
}
});
and then you can do jane.name="Jane v2.0";
.
I’ll be honest here people – I do not have a clue as to which direction to take with seemingly so many options. And just today I read Eric Elliot https://medium.com/javascript-scene/the-two-pillars-of-javascript-ee6f3281e7f3 and now I don’t know what to think anymore because he goes on to argue that people at the ES6 aren’t quite doing it right :O. Meh, I guess I’ll have to revisit the Crockfords book yet again, decide on a “one way” and see how far it takes me.
I got quite a few answers, and I also set up a bounty since my question at first didn’t get much attention. Finally the answer by user Julien Grégoire which I accepted was this one:
Using Object.create
is certainly a valid approach, but the example itself seems to me a bit misleading regarding the inner workings of Object.create
. The blog post does a really good job of summarizing the different ways to create objects in javascript, but I don’t think the example for Object.create
gives a good idea of how it works, which is more similar to the new/constructor
approach than it may seem.
Object.create
allows to create an object based on a prototype
, but without a constructor
. This means that the prototype chain
of the created object isn’t dependent on a constructor
, (which is why it may be simpler to follow, this prototype linked via the constructor is not very straightforward or easy to follow). But Object.create
still creates a prototype chain
, in the same way new
does.
So in your example, when you define name
in human
for example here:
var human = {
name: '',
And then when you create jane
:
var jane = Object.create(female, {
name: {value: 'Jane'}
You’re not really assigning a value to the name property
you defined in human
. You’re in fact adding a property to jane. But human.name
still is a property in prototype chain
of jane
. It works because javascript will follow the prototype chain to find the first matching property, but human.name
is still somehow linked to jane
.
See here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
Same thing happens if you use a constructor:
var Person = function(gender, name) {
this.name = name;
}
var newPerson = new Person();
And same goes for sayPlanet
function.
It’s a valid approach, but can lead to strange behaviors. For example, you could decide to modifiy sayPlanet
for all humans by assigning it this way:
human.sayPlanet = function(){console.log('new sayPlanet')}
This would then work for all humans
, except the ones for which you have given a sayPlanet
property of their own. Which in your case may be the expected result. But still, you have to see if sayPlanet
really should be a property of human.
With gender
, it’s applied in human
, and in male
and female
. So changing human.gender
wouldn’t work on any one. But it’s still a property of human
, which is a bit confusing when you want to work with these objects. You basically have a property that is defined, that is writable, but that when changed has no effect at all. It’s mainly an indication as to which property you need to add to your humans instances or somewhere in the prototype chain. Again, it seems to be used a lot, but when explained with this kind of examples, it somehow gives the impression that Object.create
just combines properties, but it’s not what it does.
In the end, you need to chose if you want to work with prototypes
or not. If not, then Functional inheritance is probably the best way. Then each object is different and has its own set of properties, that you can initialize, and you don’t have to worry about prototypes
.
If you want to use prototypes
, then you can use new/constructor
or Object.create
approach. But Object.create
will create a prototype chain the same way new
does, it just gets rid of the constructors.
A small example of how Object.create
and new
share some behaviors:
var human = {
name: '',
gender: '',
planetOfBirth: 'Earth',
sayGender: function () {
console.log(this.name + ' says my gender is ' + this.gender);
},
sayPlanet: function () {
console.log(this.name + ' was born on ' + this.planetOfBirth);
}
};
var male = Object.create(human, {
gender: {value: 'Male'}
});
var female = Object.create(human, {
gender: {value: 'Female'}
});
var david = Object.create(male, {
name: {value: 'David'},
planetOfBirth: {value: 'Mars', configurable: true}
});
var jane = Object.create(female, {
name: {value: 'Jane'},
sayPlanet: {value: function(){
console.log("something different");
}, writable: true, enumerable: true, configurable: true
}
});
var Male = function(name){ // in this case the real constructor is female or male. Name is the only property that 'needs' to be initialized
this.name = name;
this.planetOfBirth = 'Jupiter';
}
Male.prototype = Object.create(male);
var john = new Male('John')
david.sayGender(); // David says my gender is Male
david.sayPlanet(); // David was born on Mars
jane.sayGender(); // Jane says my gender is Female
jane.sayPlanet(); // Jane was born on Earth
john.sayGender(); // John says my gender is Female
john.sayPlanet(); // John was born on Earth
delete david.planetOfBirth; //just to show how human properties will still be in the chain even if you delete them
delete john.name; // It's true also if you use new.
delete jane.sayPlanet;
console.log('\n','----after deleting properties----');
david.sayPlanet();
jane.sayPlanet();
john.sayGender();
human.planetOfBirth = 'Venus'; // This will apply to all humans, even the ones already created
console.log('\n','----after assigning planetOfBirth on human----');
david.sayPlanet();
jane.sayPlanet();
john.sayPlanet(); // John still has planetOfBirth as its own property, since it wasn't deleted
delete john.planetOfBirth;
console.log('\n','----after deleting john planetOfBirth----');
john.sayPlanet(); // But it's still there
And in fact (just to be more confusing), some people combine Object.create
with new/constructor
or with Functional inheritance. Something like this for example:
https://john-dugan.com/object-oriented-javascript-pattern-comparison/#oloo-pattern
Applied to your example it would give something like this:
var human = {
planetOfBirth: 'Earth',
sayGender: function () {
console.log(this.name + ' says my gender is ' + this.gender);
},
sayPlanet: function () {
console.log(this.name + ' was born on ' + this.planetOfBirth);
},
init: function(name){
this.name = name;
}
};
var male = Object.create(human, {
gender: {value: 'Male'} // This is part of male/female prototype and can't be written, which seems logical
});
var female = Object.create(human, {
gender: {value: 'Female'}
});
var david = Object.create(male).init('David');
david.planetOfBirth = 'Mars';
var jane = Object.create(female).init('Jane')
jane.sayPlanet = function(){console.log('something different')};
var john = Object.create(male).init('John');
john.planetOfBirth = 'Jupiter';
david.sayGender(); // David says my gender is Male
david.sayPlanet(); // David was born on Mars
jane.sayGender(); // Jane says my gender is Female
jane.sayPlanet(); // Jane was born on Earth
john.sayGender(); // John says my gender is Female
john.sayPlanet(); // John was born on Earth
delete david.planetOfBirth; // Overridden properties will still exists after delete, but not the others.
delete john.name;
delete jane.sayPlanet;
console.log('\n','----after deleting properties----');
david.sayPlanet();
jane.sayPlanet();
john.sayPlanet();
human.planetOfBirth = 'Venus'; // This will apply to all humans, even the ones already created.
// But not for humans woth overridden planetOfBirth.
console.log('\n','----after assigning planetOfBirth on human----');
david.sayPlanet();
jane.sayPlanet();
john.sayPlanet(); // John's name is now undefinded
delete john.planetOfBirth;
console.log('\n','----after deleting john planetOfBirth----');
john.sayPlanet(); //
Not necessarily better, but it works as well, and in my opinion has certain advantages.
In any case, as others said, there doesn’t seem to be a standard or default way to do this.
Also, another great answer was by a user Nice-Guy:
The best practice as I’ve learned is to define write permissions at the Object prototype level. I learned this technique from reading Addy Osmani’s JavaScript Design Patterns, it’s very reputable and open source online: http://addyosmani.com/resources/essentialjsdesignpatterns/book/
Please check out the sayPlanet property in the example below. Keep in mind, you do not need to set all the other properties’ “writeable” to false, I only did it in my example code to illustrate the point. This approach affords more flexibility and reusability than the other, still valid, approaches. Here, you may notice that this is the Object.defineProperties
syntax within the prototype.
var human = {
name: {
value: '',
writable: false //only to illustrate default behavior not needed
},
gender: {
value: '',
writable: false //only to illustrate default behavior not needed
},
planetOfBirth: {
value: "Earth",
configurable: false //only to illustrate default behavior not needed
}
};
//here we define function properties at prototype level
Object.defineProperty(human, 'sayGender', {
value: function() {
alert(this.name + ' says my gender is ' + this.gender);
},
writable: false
});
Object.defineProperty(human, 'sayPlanet', {
value: function() {
alert(this.name + ' was born on ' + this.planetOfBirth);
},
writable: true
});
//end definition of function properties
var male = Object.create(human, {
gender: {
value: 'Male'
}
});
var female = Object.create(human, {
gender: {
value: 'Female'
}
});
var david = Object.create(male, {
name: {
value: 'David'
},
planetOfBirth: {
value: 'Mars'
}
});
var jane = Object.create(female, {
name: {
value: 'Jane'
}
});
//define the writable sayPlanet function for Jane
jane.sayPlanet = function() {
alert("something different");
};
//test cases
//call say gender before attempting to ovverride
david.sayGender(); // David says my gender is Male
//attempt to override the sayGender function for david
david.sayGender = function() {
alert("I overrode me!")
};
//I tried and failed to change an unwritable fucntion
david.sayGender(); //David maintains that my gender is Male
david.sayPlanet(); // David was born on Mars
jane.sayGender(); // Jane says my gender is Female
jane.sayPlanet(); // something different
https://twitter.com/HitmanHR/status/653822447385505792