The ultimate guide to JavaScript class pattern explains how the prototype chain works. It explains how we can chain multiple prototypes together. However, for the sake of simplicity, I left out how a put operation on an object property works. Most misunderstandings in JavaScript arises from the assumption that the put operation works similarly to the get operation. Unfortunately, that is not how it works. Let us get a bit more concrete on the topic.
Property Shadowing
Consider the following code.
const Foo = { numCalls: 0, invoke() { this.numCalls++; console.log('number of calls: ', this.numCalls); } } const foo = Object.create(Foo); foo.invoke(); // 1 const foo2 = Object.create(Foo); foo2.invoke(); // 1 console.log(Foo.numCalls); // 0
In the above code, Foo is a prototype object. We define numCalls
property on the prototype. It gives an impression that numCalls
is shared among all object instances. A get for numCalls
in any object traverses the prototype chain and gets the property from the Foo
prototype. However, our invoke method does a put operation on numCalls
property. What does the put operation do? We will expect it to traverse the prototype chain, find the numCalls
property in Foo
prototype and increment the variable. But, that is not how JavaScript works. A put operation on numCalls
property adds a new property on the objects, foo
and foo2
. It sets the value on the new property of these objects. This behaviour is called property shadowing. A new property with the same name is redefined higher up in the prototype chain. In this case, on the leaf object itself. (CodePen)
Property Setters
Let us say, we intend to increment the prototype instance. How do we avoid property shadowing? Define a property setter on the prototype.
const Foo = { invoke() { this.numCalls++; console.log('Number of calls is ', this.numCalls); } } let numCalls = 0; Object.defineProperty(Foo, 'numCalls', { get: function() { return numCalls; }, set: function(value) { numCalls = value; } }); const foo = Object.create(Foo); const foo2 = Object.create(Foo); foo.invoke(); // 1 foo2.invoke(); // 2
We verify if there is no property shadowing by getting the own property names of the objects.
console.log(Object.getOwnPropertyNames(Foo)); // ['numCalls', 'invoke'] console.log(Object.getOwnPropertyNames(foo)); // []
No new property is created in foo
object. (CodePen)
ES6 class design
ES6 class is convenient to use. But, there is also a safeguard. To avoid property shadowing, an ES6 class does NOT allow you to define any property state, but only property getters / setters.
let numCalls = 0; class Foo { get numCalls() { return numCalls; } set numCalls(value) { numCalls = value; } invoke() { this.numCalls++; console.log('Number of calls: ', this.numCalls); } } const foo = new Foo(); foo.invoke(); // 1 const foo2 = new Foo(); foo2.invoke(); // 2
Similarly, there is no new property created on the object.
console.log(Object.getOwnPropertyNames(foo)); // [] console.log(Object.getOwnPropertyNames(Foo.prototype)); // ['constructor', 'numCalls', 'invoke']
In most cases, we store the state property in the leaf objects, not on the prototype. ES6 supports this pattern by allowing state initialisation in constructors.
class Foo { constructor() { this.numCalls = 0; } invoke() { this.numCalls++; console.log('Number of calls on the object: ', this.numCalls); } } const foo = new Foo(); foo.invoke(); // 1 foo.invoke(); // 2 console.log(Object.getOwnPropertyNames(foo)); // ['numCalls'] console.log(Object.getOwnPropertyNames(Foo.prototype)); // ['constructor', 'invoke']
With constructor initialisation, the constructed object or the leaf object holds the property or state. In this case, numCalls
is present on the foo
object and not on the class or Foo
prototype. (CodePen)