I typically implement inheritance along the following lines.
function Animal () { this.x = 0; this.y = 0;}
Animal.prototype.locate = function() {
console.log(this.x, this.y);
return this;
};
Animal.prototype.move = function(x, y) {
this.x = this.x + x;
this.y = this.y + y;
return this;
}
function Duck () {
Animal.call(this);
}
Duck.prototype = new Animal();
Duck.prototype.constructor = Duck;
Duck.prototype.speak = function () {
console.log("quack");
return this;
}
var daffy = new Duck();
daffy.move(6, 7).locate().speak();
I've read this post by Eric Elliott and if I understand correctly I can use Object.create
and Object.assign
instead? Is it really that simple?
var animal = {
x : 0,
y : 0,
locate : function () {
console.log(this.x, this.y);
return this;
},
move : function (x, y) {
this.x = this.x + x;
this.y = this.y + y;
return this;
}
}
var duck = function () {
return Object.assign(Object.create(animal), {
speak : function () {
console.log("quack");
return this;
}
});
}
var daffy = duck();
daffy.move(6, 7).locate().speak();
As an aside, by convention constructor functions are capitalized, should object literals that act as constructors also be capitalized?
I realise there are many questions here discussing new
versus Object.create
, but they typically seem to relate to Duck.prototype = new Animal();
versus Duck.prototype = Object.create(Animal.prototype);
Yes, it is that simple. In your example with Object.create/Object.assign
, you are using a factory function to create new instances of duck
(similar to the way jQuery creates new instances if you select an element with var body = $('body')
). An advantage of this code style is, that it doesn't force you to call a constructor of animal
when you want to create a new duck
instance (as opposed to ES2015 Classes).
Differences in initialization
Maybe one interesting tidbit that works slightly differently than if you were to use a constructor (or any other initialization function):
When you create a duck
instace, all the properties of animal
are in the [[Prototype]]
slot of the duck
instance.
var daffy = duck();
console.log(daffy); // Object { speak: function() }
So daffy
does not have any own x
and y
properties yet. However, when you call the following, they will be added:
daffy.move(6, 7);
console.log(daffy); // Object { speak: function(), x: 6, y: 7 }
Why? In the function-body of animal.move
, we have the following statement:
this.x = this.x + x;
So when you call this with daffy.move
, this
refers to daffy
. So it will try to assign this.x + x
to this.x
. Since this.x
is not yet defined, the [[Prototype]]
chain of daffy
is traversed down to animal
, where animal.x
is defined.
Thus in the first call, the this.x
on the right side of the assignment refers to animal.x
, because daffy.x
is not defined. The second time daffy.move(1,2)
is called, this.x
on the right side will be daffy.x
.
Alternative Syntax
Alternatively, you could also use Object.setPrototypeOf
instead of Object.create/Object.assign
(OLOO Style):
var duck = function () {
var duckObject = {
speak : function () {
console.log("quack");
return this;
}
};
return Object.setPrototypeOf(duckObject, animal);
}
Naming Conventions
I'm not aware of any established conventions. Kyle Simpson uses uppercase letters in OLOO, Eric Elliot seems to use lowercase. Personally I would stick with lower-case, because the object literals that act as constructors are already fully fledged objects themselves (not just blueprint, like classes would be).
Singleton
If you only wanted a single instance (e.g. for a singleton), you could just call it directly:
var duck = Object.assign(Object.create(animal), {
speak : function () {
console.log("quack");
return this;
}
});
duck.move(6, 7).locate().speak();
I've read this post by Eric Elliott and if I understand correctly I can use
Object.create
andObject.assign
instead? Is it really that simple?
Yes, create
and assign
is much more simple because they're primitives, and less magic is going on - everything you do is explicit.
However, Eric's mouse
example is a bit confusing, as he leaves out one step, and mixes the inheritance of mouses from animals with instantiating mouses.
Rather let's try transcribing your duckling example again - let's start with doing it literally:
const animal = {
constructor() {
this.x = 0;
this.y = 0;
return this;
},
locate() {
console.log(this.x, this.y);
return this;
},
move(x, y) {
this.x += x;
this.y += y;
return this;
}
};
const duck = Object.assign(Object.create(animal), {
constructor() {
return animal.constructor.call(this);
},
speak() {
console.log("quack");
return this;
}
});
/* alternatively:
const duck = Object.setPrototypeOf({
constructor() {
return super.constructor(); // super doesn't work with `Object.assign`
},
speak() { … }
}, animal); */
let daffy = Object.create(duck).constructor();
daffy.move(6, 7).locate().speak();
You should see that what happens here is really no different from using constructors (or class
syntax for that matter), we've just stored our prototypes directly in the variables and we're doing instantiation with an explicit call to create
and constructor
.
Now you can figure that our duck.constructor
does nothing but calling its super method, so we can actually omit it completely and let inheritance do its work:
const duck = Object.assign(Object.create(animal), {
speak() {
console.log("quack");
return this;
}
});
The other thing that is often changed is the initialisation of instance properties. There is actually no reason to initialise them if we don't really need them, it's sufficient to put some default values on the prototype:
const animal = {
x: 0,
y: 0,
locate() {
console.log(this.x, this.y);
}
};
const duck = … Object.create(animal) …;
let daffy = Object.create(duck); // no constructor call any more!
daffy.x = 5; // instance initialisation by explicit assignment
daffy.locate();
The problem with this is that it only works for primitive values, and it gets repetitive. This is where factory functions get in:
function makeDuck(x, y) {
return Object.assign(Object.create(duck), {x, y});
}
let daffy = makeDuck(5, 0);
To allow for easy inheritance, the initialisation is often not done in the factory but in a dedicated method so it can be called on "subclass" instances as well. You may call this method init
, or you may call it constructor
like I did above, it's basically the same.
As an aside, by convention constructor functions are capitalized, should object literals that act as constructors also be capitalized?
If you're not using any constructors, you may assign a new meaning to capitalized variable names, yes. It might however be confusing for everyone who's not accustomed to this. And btw, they're not "object literals that act as constructors", they're just prototype objects.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With