This is the third day of my participation in the Genwen Challenge.

The lazy-loading Property Pattern in JavaScript by Nicholas C. Zakas

Typically, developers create properties in JavaScript classes for any data they may need in an instance. This is not a problem for the small amount of data that is readily available in the constructor. However, if you need to compute some data before it becomes available in the instance, you probably don’t want to incur computational overhead up front. For example, consider this class:

class MyClass {
    constructor() {
        this.data = someExpensiveComputation(); }}Copy the code

Above, the data attribute is created as a result of performing some expensive calculations. If you are not sure if you will use this property, it may not be efficient to perform the calculation up front. Fortunately, there are several ways to postpone these actions until later.

On demand property pattern

The easiest way to optimize computationally expensive operations is to wait until the data is needed. For example, you can use accessor properties with getters to perform calculations on demand, as follows:

class MyClass {
    get data() {
        returnsomeExpensiveComputation(); }}Copy the code

In this case, the calculation is not performed until someone reads the data attributes for the first time, which is an improvement. However, every time the data attribute is accessed, the computation overhead is the same, and it is even worse than the previous example where the computation was performed at least once. This is not a very good solution, but you can build on it to create a better one.

Messy lazy load property pattern

Performing computations only when attributes are accessed is a good place to start. What you really need is to cache the information after that and use only the cached version after that. But where do you cache this information for easy access? The simplest way is to define a property with the same name and set its value to calculated data, as follows:

class MyClass {
    get data() {
        const actualData = someExpensiveComputation();

        Object.defineProperty(this."data", {
            value: actualData,
            writable: false.configurable: false.enumerable: false
        });

        returnactualData; }}Copy the code

Above, the data property is again defined as a getter in the class, but this time it caches the results. The call to object.defineProperty () creates a new property, data, which has a fixed actualData value and is set to non-writable, non-configurable, and non-enumerable (to match getters). The value itself is then returned. The next time the data property is accessed, it reads the value of the recently created property instead of calling the getter:

const object = new MyClass();

// calls the getter
const data1 = object.data;

// reads from the data property
const data2 = object.data;
Copy the code

In fact, all calculations are done only when the data attributes are first read. Each subsequent read of a data attribute returns the cached version.

One disadvantage of this pattern is that data attributes start as non-enumerable stereotype attributes and end as non-enumerable instance attributes:

const object = new MyClass();
console.log(object.hasOwnProperty("data"));     // false

const data = object.data;
console.log(object.hasOwnProperty("data"));     // true
Copy the code

While this distinction is not important in many cases, it is important to understand this pattern because it can cause subtle problems when objects are passed. Fortunately, it’s easy to solve this problem with improved patterns.

There are two types of attributes: data attributes and accessor attributes. Both of them are configured with different signals and enumerable. The difference is that the data property is a value and writable, and the accessor property is a setter and getter. They cannot exist at the same time, and an error is reported if both the data and accessor properties are set. The second call sets data as a non-configurable data property, meaning object.defineProperty will not be executed the next time.

Class’s unique lazy-loaded property pattern

If you have a scenario where lazy-loaded attributes always exist in the instance itself, you can use Object.defineProperty() to create attributes in the class constructor. It’s messier than the previous example, but it ensures that the property only exists in the instance. Here’s an example:

class MyClass {
    constructor() {

        Object.defineProperty(this."data", {
            get() {
                const actualData = someExpensiveComputation();

                Object.defineProperty(this."data", {
                    value: actualData,
                    writable: false.configurable: false
                });

                return actualData;
            },
            configurable: true.enumerable: true}); }}Copy the code

Above, the constructor creates the accessor property using Object.defineProperty(). The property is created in the instance (by using this) and a getter is defined and the property is specified as enumerable and configurable (typically an instance’s own property). It is especially important to set data as a configurable property so that you can call Object.defineProperty() again next time.

The getter performs the calculation and calls Object.defineProperty() again. The data property is now redefined as a data property that is specified with explicit values and configured to be writable and unconfigurable to protect the final data. The calculated data is then returned from the getter. The next time data is accessed, it reads from the cache. In addition, the data property now exists only as an instance property and behaves the same before and after the first read:

const object = new MyClass();
console.log(object.hasOwnProperty("data"));     // true

const data = object.data;
console.log(object.hasOwnProperty("data"));     // true
Copy the code

For classes, this is probably the pattern you want to use, while for object literals you can use a simpler approach.

Lazy load property pattern for object literals

If you use object literals instead of classes, the process is much simpler, because getters defined on object literals are defined as enumerable properties of themselves (rather than stereotype properties), just like data properties. This means that you can use a “messy lazy-loaded property pattern” for classes without chaos:

const object = {
    get data() {
        const actualData = someExpensiveComputation();

        Object.defineProperty(this."data", {
            value: actualData,
            writable: false.configurable: false.enumerable: false
        });

        returnactualData; }};console.log(object.hasOwnProperty("data"));     // true

const data = object.data;
console.log(object.hasOwnProperty("data"));     // true
Copy the code

conclusion

The ability to redefine object attributes in JavaScript provides a unique opportunity to cache potentially computationally expensive information. By starting with accessor properties that are redefined as data properties, you can defer computation until the first time the properties are read, and then cache the results for later use. This works for both classes and object literals, and is easier in object literals because you don’t have to worry about your getter ending up on the prototype.

One of the best ways to improve performance is to avoid doing the same work twice, so any time you can cache results for later use, you can speed up your program. Techniques such as the lazy-loaded attribute pattern allow any attribute to become a caching layer to improve performance.