There are 4 ways to create new objects in JavaScript:
- Object initializers, also known as literal notation
Object.create
- Constructors
- ES6 classes
Depending on which method you choose, the newly created object will have a different prototype chain1.
1. Object initializers
let x = { a: 1 };
Object.prototype.isPrototypeOf(x); // true
Objects created in this manner will have Object.prototype
as its top-level prototype:
x => Object.prototype
Arrays and functions also have their own literal notation:
let y = [1, 2, 3];
Array.prototype.isPrototypeOf(y); // true
Object.prototype.isPrototypeOf(Array.prototype); // true
let z = () => {}; // ES6 fat arrow syntax
Function.prototype.isPrototypeOf(z); // true
Object.prototype.isPrototypeOf(Function.prototype); // true
In these cases, y
’s and z
’s prototype chains will be
y => Array.prototype => Object.prototype
and
z => Function.prototype => Object.prototype
respectively.
2. Object.create
Object.create
takes in an arbitrary object (or null
) as a first argument, which will be the prototype of the new object2.
let x = {
a: 1,
};
let y = Object.create(x);
y.a === 1; // true
x.isPrototypeOf(y); // true
Object.prototype.isPrototypeOf(x); // true
Thus, y
’s prototype chain is:
y => x => Object.prototype
Object.create
is actually quite special because any arbitrary object can be specified as the prototype, so we can do otherwise nonsensical things such as:
let x = [1, 2, 3];
let y = Object.create(x);
y.forEach; // is valid, returns function forEach()
x.isPrototypeOf(y); // true
In this case, y
’s prototype chain will be:
y => x => Array.prototype => Object.prototype
3. Constructors
When a3 function Thing
is invoked with the new
keyword, as in let x = new Thing()
, it behaves as a constructor function, which means the following things will happen:
- A new, empty object is created, whose prototype is Thing.prototype (the prototype object of the
Thing
function object) - The body of the function
Thing
is executed, with itsthis
set to the new empty object - The return value of the
Thing
function is the result of thenew Thing()
expression, unless no return value is specified, then the new object is returned
function Thing() {}
let z = new Thing();
Thing.prototype.isPrototypeOf(z); // true
To highlight the fact that the prototype
property object is distinct from the object to which it belongs to, notice the following:
Function.prototype.isPrototypeOf(Thing); // true
Object.prototype.isPrototypeOf(Thing); // true
Function.prototype.isPrototypeOf(Thing.prototype); // false
Object.prototype.isPrototypeOf(Thing.prototype); // true
If we think of Thing.prototype
as simply an object, this shouldn’t come as a surprise. In fact, if we were do something like this:
Object.prototype.a = 1;
Function.prototype.b = 2;
z.a; // 1
z.b; // undefined
Thus, z
’s prototype chain looks like:
z => Thing.prototype => Object.prototype
and not
z => Thing.prototype => Function.prototype => Object.prototype
ES6 Classes
Prototype chains in ES6 classes behave almost exactly like constructors (that is because classes are syntactic sugar around constructors):
class Thing {
a() {
return 1;
}
b() {
return 2;
}
}
class AnotherThing extends Thing {
b() {
return 3;
}
c() {
return 4;
}
}
let x = new AnotherThing();
x.c = () => {
return 5;
};
x.a(); // 1
x.b(); // 3
x.c(); // 5
AnotherThing.prototype.isPrototypeOf(x); // true
Thing.prototype.isPrototypeOf(AnotherThing.prototype); // true
Thus, x
’s prototype chain is:
x => AnotherThing.prototype => Thing.prototype => Object.prototype
And of course, as mentioned earlier, classes really are just syntactic sugar for constructors:
Thing.isPrototypeOf(AnotherThing); // true
Function.prototype.isPrototypeOf(Thing); // true
Function.prototype.isPrototypeOf(AnotherThing); // true
See footnote4 for a little more detail on how subclassing with extends
actually works and how it affects the prototype chain between the subclass and the superclass.
#Footnotes
-
I’ve used
isPrototypeOf
here for better readability, but you can also substitute it for its inverse,__proto__
, like↩x.__proto__ === Object.prototype; // true
-
And another optional object as a second argument that specifies property descriptors. ↩
-
When I mean “a”, I actually mean any arbitrary function. Of course, functions meant to be used as useful constructors should look a certain way. ↩
-
Part of Babel’s transpiled output for
extends
includes a_inherits
function, the full body of which is below:function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError( "Super expression must either be null or a function, not " + typeof superClass ); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true, }, }); if (superClass) subClass.__proto__ = superClass; }
_inherits
explicitly creates the subclass’s prototype object usingObject.create
, specifying the super class’s prototype as its prototype. It also sets the subclass’s__proto
’s property to the superclass. ↩