4

I'm reading up on JavaScript's virtual getter using Mozilla's documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get

In it there's a section with some example code:

In the following example, the object has a getter as its own property. On getting the property, the property is removed from the object and re-added, but implicitly as a data property this time. Finally, the value gets returned.

get notifier() {
  delete this.notifier;
  return this.notifier = document.getElementById('bookmarked-notification-anchor');
},

This example comes right after the article talks about lazy/smart/memoized, but I am not seeing how the code is an example of a lazy/smart/memoized getter.

Or is that section talking about something else completely?

It just feels like I'm not following the flow of the article and it might be because I do not understand some key concept.

Please let me know if I'm just over-thinking this and that section was just shoehorned in or if the section really does relate to lazy/smart/memoized somehow.

Thank you for your guidance

Update 1:
I guess maybe I don't know how to verify that the code is getting memoized.

I tried to run this in the IDE on the page:

const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    if (this.log.length === 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  },
  get notifier() {
    delete this.notifier;
    return this.notifier = document.getElementById('bookmarked-notification-anchor');
  },
};

console.log(obj.latest);
// expected output: "c"
console.log(obj.notifier);  // returns null

This seems more appropriate, but I can't verify that the cache is being used:

const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    if (this.log.length === 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  },
  get notifier() {
    delete this.notifier;
    return this.notifier = this.log;
  },
};

console.log(obj.latest);
// expected output: "c"
console.log(obj.notifier); // Array ["a", "b", "c"]
console.log(obj.notifier); // Array ["a", "b", "c"]

I guess I'm not sure why does the property need to be deleted first, delete this.notifier;? Wouldn't that invalidate the cache each time?

Update 2: @Bergi, thanks for the suggested modifications to the example code.

I ran this (with the delete):

const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    if (this.log.length === 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  },
  get notifier() {
    delete this.notifier;
    return this.notifier = console.log("heavy computation");
  },
};

console.log(obj.latest);
// expected output: "c"
obj.notifier;
obj.notifier;

and got:

> "c"
> "heavy computation"

I ran this (without the delete):

const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    if (this.log.length === 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  },
  get notifier() {
    //delete this.notifier;
    return this.notifier = console.log("heavy computation");
  },
};

console.log(obj.latest);
// expected output: "c"
obj.notifier;
obj.notifier;

and got:

> "c"
> "heavy computation"
> "heavy computation"

So that definitely, proves memoization is happening. Maybe there's too much copying and pasting updates to the post, but I'm having a hard time understanding why the delete is necessary to memoize. My scattered and naive brain is thinking that the code should memoize when there is no delete.

Sorry, I'll need to sit and think about it some. Unless you have a quick tip on how to understand what's going on.

Thank you again for all of your help

Update 3:
I guess I'm still missing something.

From Ruby, I understand memoization as:
if it exists/pre-calculated, then use it; if it does not exist, then calculate it

something along the lines of:
this.property = this.property || this.calc()

With the delete in the example code snippet, wouldn't the property always not exist and, hence, would always need to be recalculated?

There's definitely something wrong with my logic, but I am not seeing it. I guess maybe it's a "I don't know what I don't know" scenario.

Zhao Li
  • 4,936
  • 8
  • 33
  • 51
  • "*I am not seeing how the code is an example of a memoized getter.*" - what else do you think the code? Did you step through what happens when you access the `.notifier` property twice? The example absolutely belongs into that section. – Bergi Jun 27 '21 at 19:09
  • @Bergi thanks for letting me know the section is an example of memoization. Maybe I'm so new to this that I'm not asking the question properly. I tried to add more details in the "Update" of the original question. Hopefully, that will help bridge the gap on where my knowledge gap is. Thank you again for your time – Zhao Li Jun 27 '21 at 19:29
  • Can you edit your post to not be two completely different posts, one above the other? Make it one, coherent post, especially if you're updating it through edits. No "edit: ...", no "update for ...", keep the post one post. You're not just posting "for you, now", you're posting for yourself _and all future readers_. – Mike 'Pomax' Kamermans Jun 27 '21 at 19:36
  • @ZhaoLi Simply replace the `document.getElementById('bookmarked-notification-anchor');` with a `console.log('Heavy computation')` and see how often you get the log that the heavy computation ran. – Bergi Jun 27 '21 at 19:37
  • @ZhaoLi "*I'm not sure why does the property need to be deleted first*" - try without the `delete` statement and you'll see. An alternative would be to use `Object.defineProperty` to redefine it as a data property, but that might not be as short. – Bergi Jun 27 '21 at 19:38
  • @Mike'Pomax'Kamermans thank you for the pointer. When I was first updating, I was debating whether top or bottom was better. After your comment, it makes sense that bottom is better. Thanks for the pointer. I've updated the post for the updates to be on the bottom. – Zhao Li Jun 27 '21 at 19:58
  • @Bergi thank you for guiding me to this point. I have an idea of how to "see" the effects. Now, I have to sit and figure out why. Right now, it is counter-intuitive that the having the `delete` makes the code memoize, while removing the `delete` makes the code not memoize. Thank you again for all of your help – Zhao Li Jun 27 '21 at 19:59
  • @ZhaoLi Oh, sorry. The educational effect I was aiming for by removing the `delete` only works in strict mode :-/ Try prepending `"use strict"` to the script or the getter body. (This actually is a real-world issue with the `delete`, when the memoised getter is inherited. See [here](https://stackoverflow.com/q/37977946/1048572) and the related links for more discussion) – Bergi Jun 27 '21 at 20:23
  • @Bergi thanks, I'll take a look at the link and post back. – Zhao Li Jun 27 '21 at 20:26
  • 1
    @ZhaoLi again: don't add "update...", you have a _long_ post, so rewrite the whole post to reflect your current problem. If someone recommends something, and you try it, and that changes your code/setup/etc, your question is now _a different question_ and shouldn't still mention any of the no-longer-relevant bits of the original post. – Mike 'Pomax' Kamermans Jun 27 '21 at 20:46
  • 1
    Apparently this is a subject I like talking about (and we haven't even started talking about [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), [well known Symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#static_properties), [Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect), ...). So tell me if you have any questions! – Sheraff Jun 27 '21 at 21:59
  • 2
    @Mike'Pomax'Kamermans thanks for the insight. If the updates has changed the question drastically enough for it to be rewritten, what about creating a new question and choosing the most relevant answer that led to the follow up question? Is that usually recommended or frowned upon on StackOverflow? – Zhao Li Jun 28 '21 at 09:17
  • 1
    If your question becomes effectively a completely different question, because someone's hints solved your original issue, posting a new question for the new situation is 100% allowed and encouraged. And then you typically want to write an answer that explains how the original issue got solved if there wasn't one yet (or delete the original question if you think no one else will ever run into the same thing) – Mike 'Pomax' Kamermans Jun 28 '21 at 17:11

2 Answers2

5

how to test memoization

An easy way to test whether something is getting memoized is with Math.random():

const obj = {
    get prop() {
        delete this.prop
        return this.prop = Math.random()
    }
}

console.log(obj.prop) // 0.1747926550503922
console.log(obj.prop) // 0.1747926550503922
console.log(obj.prop) // 0.1747926550503922

If obj.prop wasn't getting memoized, it would return a random number every time:

const obj = {
    get prop() {
        return Math.random()
    }
}

console.log(obj.prop) // 0.7592929509653794
console.log(obj.prop) // 0.33531447188307895
console.log(obj.prop) // 0.685061719658401

how does it work

What happens in the first example is that

  • delete removes the property definition, including the getter (the function currently being executed) along with any setter, or all the extra stuff along with it;
  • this.prop = ... re-creates the property, this time more like a "normal" one we're used to, so next time it's accessed it doesn't go through a getter.

So indeed, the example on MDN demonstrates both:

  • a "lazy getter": it will only compute the value when the value is needed;
  • and "memoization": it will only compute it once and then just return the same result every time.

in depth explanation of object properties

So you can understand better what happens when we first declare our object, our getter, when we delete, and we then re-assign the property, I'm going to try and go a little more in depth into what object properties are. Let's take a basic example:

const obj = {
  prop: 2
}

In this case, we can get the "configuration" of this property with getOwnPropertyDescriptor:

console.log(Object.getOwnPropertyDescriptor(obj, 'prop'))

which outputs

{
    configurable: true,
    enumerable: true,
    value: 2,
    writable: true,
}

In fact, if we wanted to be unnecessarily explicit about it, we could have defined our obj = { prop: 2 } another (equivalent but verbose) way with defineProperty:

const obj = {}
Object.defineProperty(obj, 'prop', {
    configurable: true,
    enumerable: true,
    value: 2,
    writable: true,
})

Now when we defined our property with a getter instead, it was the equivalent of defining it like this:

Object.defineProperty(obj, 'prop', {
    configurable: true,
    enumerable: true,
    get() {
        delete obj.prop
        return obj.prop = Math.random()
    }
})

And when we execute delete this.prop, it removes that entire definition. In fact:

console.log(obj.prop) // 2
delete obj.prop
console.log(Object.getOwnPropertyDescriptor(obj, 'prop')) // undefined

And finally, this.prop = ... re-defines the property that was just removed. It's a russian doll of defineProperty.

Here's what this would look like with all of the entirely unnecessarily explicit definitions:

const obj = {}
Object.defineProperty(obj, 'prop', {
    enumerable: true,
    configurable: true,
    get() {
        const finalValue = Math.random()
        Object.defineProperty(obj, 'prop', {
            enumerable: true,
            configurable: true,
            writable: true,
            value: finalValue,
        })
        return finalValue
    },
})

bonus round

Fun fact: it is a common pattern in JS to assign undefined to an object property we want to "delete" (or pretend it was never there at all). There is even a new syntax that helps with this called the "Nullish coalescing operator" (??):

const obj = {}

obj.prop = 0
console.log(obj.prop) // 0
console.log(obj.prop ?? 2) // 0

obj.prop = undefined
console.log(obj.prop) // undefined
console.log(obj.prop ?? 2) // 2

However, we can still "detect" that the property exists when it is assigned undefined. Only delete can really remove it from the object:

const obj = {}

console.log(obj.prop) // undefined
console.log(obj.hasOwnProperty('prop')) // false
console.log(Object.getOwnPropertyDescriptor(obj, 'prop')) // undefined

obj.prop = undefined
console.log(obj.prop) // undefined
console.log(obj.hasOwnProperty('prop')) // true
console.log(Object.getOwnPropertyDescriptor(obj, 'prop')) // {"writable":true,"enumerable":true,"configurable":true}

delete obj.prop
console.log(obj.prop) // undefined
console.log(obj.hasOwnProperty('prop')) // false
console.log(Object.getOwnPropertyDescriptor(obj, 'prop')) // undefined
Sheraff
  • 5,730
  • 3
  • 28
  • 53
0

@sheraff's answer led me to the following understanding. Hopefully, this re-phrasing of @sheraff's answer is useful for others.

The code from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get:

get notifier() {
  delete this.notifier;
  return this.notifier = document.getElementById('bookmarked-notification-anchor');
},

is not demonstrating getter's/setter's memoization behavior, but rather, it is implementing memoization while using getters/setters.

As the article states:

Note that getters are not “lazy” or “memoized” by nature; you must implement this technique if you desire this behavior.

The following example code snippets helped me realize this point.

This is a working snippet based off of the example code provided in the article:

const obj = {
  get notifier() {
    console.log("getter called");
    delete this.notifier;
    return this.notifier = Math.random();
  },
};

console.log(obj.notifier);
console.log(obj.notifier);
// results in
> "getter called"
> 0.644950142066832
> 0.644950142066832

This is a more verbose version of the code that simplifies some of the JavaScript tricks so it is easier for me to understand (@sheraff's answer helped me get to the comments on this code):

const obj = {
  get notifier() {
    console.log("getter called");
    delete this.notifier; // remove this getter function from the object so Math.random() won't be called again
    let value = Math.random(); // this resource intensive operation will be skipped on subsequent calls to obj.notifier
    this.notifier = value; // create a new property of the same name that does not have getter/setter
    return this.notifier; // return new property so the first call to obj.notifier does not return undefined
  },
};

console.log(obj.notifier);
console.log(obj.notifier);
// results in
> "getter called"
> 0.7212959093641651
> 0.7212959093641651

This is how I would have implemented memoization if I had never read the article and coming from another programming language:

const obj = {
  get notifier() {
    if (this._notifier) {
      return this._notifier;
    } else {
      console.log("calculation performed");
      return this._notifier = Math.random();
    }
  },
};

console.log(obj.notifier);
console.log(obj.notifier);
// results in
> "calculation performed"
> 0.6209661598889056
> 0.6209661598889056

There are probably inefficiencies and other reasons I'm not aware of that should deter from implementing memoization this way, but it is simple and straight forward for me to understand. This is what I had expected to see in the article for implementing memoization. Not seeing it, I for some reason thought that the article was merely demonstrating memoization.

All of the above code snippets implement memoization, which is the intent of the example code in the article. My initial confusion was misreading that the code was demonstrating memoization which made the delete statement seem odd to me.

Hope that helps

Zhao Li
  • 4,936
  • 8
  • 33
  • 51