Tweet less. Write more!

Justin Johansson's 140-character expander


TL;WM Lazy values in ES2015 classes

MDN describes a technique for implementing lazy values in JavaScript as an use case for getters. This article expands on the example presented on MDN with the technique adapted for ES2015 classes.

Today I was playing with an idea for resolving circular references in JavaScript objects and a use case for JavaScript getters, namely implementing lazy values, came to mind. It's covered on MDN on this page in the examples section, "Smart / self-overwriting / lazy getters".

Anyway, the gist is this. If computing a constant value is expensive, it is pretty much pointless and only contributes to global warming to compute such value unless it is actually used. In this scenario a getter can be used in a fashion such that the value is computed upon demand, namely, when the getter is invoked. Once calculated, the getter is removed from its owner object and replaced with a property of the same name which is associated with the calculated value. The next time the value is demanded, since it has already been calculated there is no need to calculate it again.

In the console output below, note that the trace print for calculating the value appears just once, when the value is first demanded and not again for subsequent references.

Code

const MathValues = {

  get PI_TO_1000_DECIMAL_PLACES() {
    console.log("Calculating PI_TO_1000_DECIMAL_PLACES");
    result = "3.1415926536....";  // calculate it somehow
    delete this.PI_TO_1000_DECIMAL_PLACES;
    return this.PI_TO_1000_DECIMAL_PLACES = result;
  }

}

console.log("MathValues.PI_TO_1000_DECIMAL_PLACES", MathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("MathValues.PI_TO_1000_DECIMAL_PLACES", MathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("MathValues.PI_TO_1000_DECIMAL_PLACES", MathValues.PI_TO_1000_DECIMAL_PLACES);

Console output

Calculating PI_TO_1000_DECIMAL_PLACES
MathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....
MathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....
MathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....

Now let's try the same thing as a getter method on an ES2015 class and see what happens.

Code

class MathValues {

  get PI_TO_1000_DECIMAL_PLACES() {
    console.log("Calculating PI_TO_1000_DECIMAL_PLACES");
    let result = "3.1415926536....";  // calculate it somehow
    delete this.PI_TO_1000_DECIMAL_PLACES;
    return this.PI_TO_1000_DECIMAL_PLACES = result;
  }

}

const mathValues = new MathValues();
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);

Console output

TypeError: Cannot set property PI_TO_1000_DECIMAL_PLACES of #<MathValues> which has only a getter

Okay, it looks like the game rules have changed and we will have to supply a dummy setter to pair off with the getter to get [yes, pun] this to run without throwing a TypeError. (I'm testing this on Node JS v7.3.0.)

This time the code runs cleanly but the Pi calculation executes every time the value is requested, indicating that our strategy is not working.

Code

class MathValues {

  get PI_TO_1000_DECIMAL_PLACES() {
    console.log("Calculating PI_TO_1000_DECIMAL_PLACES");
    let result = "3.1415926536....";  // calculate it somehow
    delete this.PI_TO_1000_DECIMAL_PLACES;
    return this.PI_TO_1000_DECIMAL_PLACES = result;
  }

  set PI_TO_1000_DECIMAL_PLACES(dummy) {}

}

const mathValues = new MathValues();
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);

Console output

Calculating PI_TO_1000_DECIMAL_PLACES
mathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....
Calculating PI_TO_1000_DECIMAL_PLACES
mathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....
Calculating PI_TO_1000_DECIMAL_PLACES
mathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....

So what's the deal? My guess is that it's something to do with the getter actually residing on the prototype chain of the object that was instantiated by the class MathValue. Working on that hunch, let's call upon the versatile, but not often used, Object.defineProperty ammo.

Code

class MathValues {

  get PI_TO_1000_DECIMAL_PLACES() {
    console.log("Calculating PI_TO_1000_DECIMAL_PLACES");
    let result = "3.1415926536....";  // calculate it somehow

    Object.defineProperty(this, "PI_TO_1000_DECIMAL_PLACES", {
      value: result,
      enumerable: true,
      configurable: false,
      writable: false
    });

    return result;
  }

  set PI_TO_1000_DECIMAL_PLACES(dummy) {}

}

const mathValues = new MathValues();
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);
console.log("mathValues.PI_TO_1000_DECIMAL_PLACES", mathValues.PI_TO_1000_DECIMAL_PLACES);

Console output

Calculating PI_TO_1000_DECIMAL_PLACES
mathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....
mathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....
mathValues.PI_TO_1000_DECIMAL_PLACES 3.1415926536....

Lo and behold, it works! As in the first example beginning this article which uses a literal object, the "expensive" Pi calculation is called upon once and once only. The computed value has been effectively memoised for immediate retrieval by subsequence references and we can declare victory! Now that we know the ceremony required to achieve the above, all that's needed is a utility function to tuck the functionality away for reuse.

This concludes my first TL;WM (Tweet less; Write more) article. I'd love to get your 140-character feedback via the Tweet button below. Thanks for reading & sharing :D

Update 2017-01-20

Turns out there was a question about the very subject of this article on StackOverflow not that long ago. Unsurprisingly, my solution coincides with the accepted answer!