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__
, likejs 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. ↩