The first thing you should understand is that the snippet you've provided as an example is still prototypal inheritance, and here's why:
Human
is a function which contains a prototype
object. Instances of Human
extend that prototype object with their own data initialized in the Human
constructor.
- The
prototype
object can be modified at runtime. Even after you've created instances of the class, you can still modify their inherited behavior by adding or changing properties on the prototype
object. None of this is possible with classical inheritance.
- In classical inheritance, there is a distinct difference between a class and an object. In prototypal inheritance, classes are merely an object that is a constructable function, meaning it can be invoked with the
new
keyword, but otherwise, can be treated like any other object.
Given this information, let's demonstrate a few key similarities and differences between Object.create()
and new
:
function Human() {
this.eyes = 2;
this.feet = 2;
}
Human.prototype.walk = function () { };
var josh = new Human();
console.log(josh);
var human = {
constructor: function Human() {
this.eyes = 2;
this.feet = 2;
},
walk: function () { }
};
// create josh with prototype of human
var josh = Object.create(human);
// initialize own properties by calling constructor
human.constructor.call(josh); // or josh.constructor();
console.log(josh);
It may not look it at first, but these two snippets are actually creating an instance josh
with the exact same layout:
{
eyes: 2,
feet: 2,
__proto__: {
walk: f (),
constructor: f Human(),
__proto__: Object.prototype
}
}
That is to say:
var proto = Object.getPrototypeOf(josh);
var protoProto = Object.getPrototypeOf(proto);
console.log(proto === Human.prototype); // or proto === human
console.log(protoProto === Object.prototype);
<- true
<- true
This demonstrates the prototype chain of josh
. It is the path of inheritance that determines the behavior of the object, and shows that josh
inherits from Human
, which inherits from Object
.
The difference between the two Stack Snippet consoles above is due to the fact that the first snippet's constructor
is a non-enumerable property of Human.prototype
, while the second snippet's constructor
is an enumerable property of human
.
If you want to pick apart the second snippet, I highly suggest taking a closer look at the documentation for Object.create()
on MDN, and possibly even glancing at the specification for it if you can grok the dense language.
Here's how you can use Object.create()
with our definition of Human
instead:
function Human() {
this.eyes = 2;
this.feet = 2;
}
Human.prototype.walk = function () { };
// create prototypal inheritance
var josh = Object.create(Human.prototype);
// initialize own properties
Human.call(josh); // or josh.constructor();
console.log(josh);
This initializes the instance properties of the instance josh
by calling the ES5 constructor with josh
as the context (the this
keyword).
Lastly, since it was mentioned in comments, all of this can be abstracted for simplicity using the ES6 class
keyword, which still uses prototypal inheritance:
class Human {
constructor() {
this.eyes = 2;
this.feet = 2;
}
walk() { }
}
var josh = new Human();
console.log(josh);
The output may appear different but if you check in the real Developer Console, you'll find that the only difference in the layout of josh
is due to the fact that ES6 classes declare member methods like walk()
as non-enumerable properties of Human.prototype
, which is why it doesn't show up in the Stack Snippet console.
You cannot use Object.create()
the same way as demonstrated in ES5 because an ES6 class
is only constructable (invoke-able with new
) and not callable (invoke-able without new
):
class Human {
constructor() {
this.eyes = 2;
this.feet = 2;
}
walk() { }
}
var josh = Object.create(Human.prototype); // still works
// no own properties yet because the constructor has not been invoked
console.log(josh);
// cannot call constructor to initialize own properties
Human.call(josh); // josh.constructor(); would not work either
Addendum
I tried to come up with a way to more easily see the prototype chain of objects in the Stack Snippet console, so I wrote this function layout()
. It recurses into an object's prototype chain and makes all the properties enumerable. Since prototype chains cannot have cycles, this can never get stuck in infinite recursion:
// makes all properties in an object's prototype chain enumerable
// don't worry about understanding this implementation
function layout (o) {
if (typeof o !== 'object' || !o || o === Object.prototype) return o;
return [...Object.getOwnPropertyNames(o), '__proto__'].reduce(
(a, p) => Object.assign(a, { [p]: layout(o[p]) }),
Object.create(null)
);
}
// this is intentionally one line in order to
// make Stack Snippet Console output readable
function HumanES5() { this.eyes = 2; this.feet = 2; }
HumanES5.prototype.walk = function () { };
var josh = new HumanES5();
console.log(layout(josh));
var josh = Object.create(HumanES5.prototype);
HumanES5.call(josh); // or josh.constructor();
console.log(layout(josh));
class HumanES6 {
constructor () { this.eyes = 2; this.feet = 2; }
walk () { }
}
var josh = new HumanES6();
console.log(layout(josh));
var josh = Object.create(HumanES6.prototype);
// HumanES6.call(josh); will fail, remember?
console.log(layout(josh));
.as-console-wrapper{min-height:100%!important}
There are two things to note here.
- In the last two outputs,
class HumanES6 { ... }
actually refers to the constructor
function in the class declaration. In prototypal inheritance, the class and its constructor are synonymous.
- The last output doesn't have the own properties
eyes
and feet
since the constructor
was never invoked to initialize that instance of josh
.