Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the example code in Mozilla's virtual getter documentation trying to show?

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.

like image 208
Zhao Li Avatar asked Oct 28 '25 01:10

Zhao Li


1 Answers

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
like image 173
Sheraff Avatar answered Oct 29 '25 16:10

Sheraff