How to override a function when creating a new object in the prototypal inheritance?
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 theObject.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 ofObject.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 forObject.create
gives a good idea of how it works, which is more similar to thenew/constructor
approach than it may seem.
Object.create
allows to create an object based on aprototype
, but without aconstructor
. This means that theprototype chain
of the created object isn’t dependent on aconstructor
, (which is why it may be simpler to follow, this prototype linked via the constructor is not very straightforward or easy to follow). ButObject.create
still creates aprototype chain
, in the same waynew
does.So in your example, when you define
name
inhuman
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 inhuman
. You’re in fact adding a property to jane. Buthuman.name
still is a property inprototype chain
ofjane
. It works because javascript will follow the prototype chain to find the first matching property, buthuman.name
is still somehow linked tojane
.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 asayPlanet
property of their own. Which in your case may be the expected result. But still, you have to see ifsayPlanet
really should be a property of human.With
gender
, it’s applied inhuman
, and inmale
andfemale
. So changinghuman.gender
wouldn’t work on any one. But it’s still a property ofhuman
, 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 thatObject.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 aboutprototypes
.If you want to use
prototypes
, then you can usenew/constructor
orObject.create
approach. ButObject.create
will create a prototype chain the same waynew
does, it just gets rid of the constructors.A small example of how
Object.create
andnew
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 thereAnd in fact (just to be more confusing), some people combine
Object.create
withnew/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 differenthttps://twitter.com/HitmanHR/status/653822447385505792
Leave a Comment