How does property shadowing in JavaScript affect ES6 class design?

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']

CodePen

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)

Related Posts

Leave a Reply

Your email address will not be published.