Mozilla 的虚拟 getter 文档中试图显示的示例代码是什么?

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

我正在使用 Mozilla 的文档阅读 JavaScript 的虚拟 getter:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get

其中有一节包含一些示例代码:

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');
},

这个例子是在文章讨论 lazy/smart/memoized 之后出现的,但我没有看到代码如何成为 lazy/smart/memoized getter.

的例子

或者那部分完全是在谈论别的东西?

感觉跟不上文章的流程,可能是一些关键概念没看懂

请让我知道我是否只是想多了这个和那个部分只是硬塞进去,或者该部分是否真的与 lazy/smart/memoized 有某种关联。

感谢您的指导

更新 1:
我想也许我不知道如何验证代码是否已被记忆。

我试图在 IDE 页面上 运行 这个:

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

这似乎更合适,但我无法验证是否正在使用缓存:

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

我想我不确定为什么 属性 需要先删除,delete this.notifier;?那不是每次都让缓存失效吗?

更新 2: @Bergi,感谢您对示例代码提出的修改建议。

我运行这个(用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;

并得到:

> "c"
> "heavy computation"

我运行这个(没有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;

并得到:

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

所以这绝对证明记忆化正在发生。也许 post 的复制和粘贴更新太多了,但我很难理解为什么需要 delete 来记忆。我那散乱幼稚的脑子在想代码应该在没有delete.

的情况下memoize

抱歉,我需要坐下来考虑一下。除非你有关于如何理解正在发生的事情的快速提示。

再次感谢您的帮助

更新 3:
我想我仍然缺少一些东西。

来自Ruby,我对记忆的理解是:
如果是 exists/pre-calculated,则使用它;如果不存在,则计算

大致如下:
this.property = this.property || this.calc()

对于示例代码片段中的 delete,属性 不会一直不存在,因此总是需要重新计算吗?

我的逻辑肯定有问题,但我没有看到。我想这可能是一个“我不知道我不知道”的场景。

如何测试记忆

一个简单的测试是否有记忆的方法是 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

如果 obj.prop 没有被记忆,它每次都会 return 一个随机数:

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

它是如何工作的

第一个例子中发生的事情是

  • delete 删除 属性 定义,包括 getter(当前正在执行的函数)以及任何 setter,或 all the extra stuff它;
  • this.prop = ... 重新创建 属性,这次更像是我们习惯的“正常”,所以下次访问它时不会经过 getter.

的确如此,MDN 上的示例证明了两者:

  • a "lazy getter": 它只会计算值 需要该值时;
  • 和“记忆”:它只会计算一次然后每次return相同的结果。

对象属性的深入解释

所以你可以更好地理解当我们第一次声明我们的对象时会发生什么,我们的 getter,当我们删除,然后我们重新分配 属性,我会尝试更深入地了解什么是对象属性。让我们举一个基本的例子:

const obj = {
  prop: 2
}

在这种情况下,我们可以用getOwnPropertyDescriptor得到这个属性的“配置”:

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

输出

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

事实上,如果我们想对其进行不必要的明确说明,我们可以用 defineProperty:

定义我们的 obj = { prop: 2 } 另一种(等效但冗长的)方式
const obj = {}
Object.defineProperty(obj, 'prop', {
    configurable: true,
    enumerable: true,
    value: 2,
    writable: true,
})

现在,当我们用 getter 定义 属性 时,相当于这样定义它:

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

当我们执行 delete this.prop 时,它会删除整个定义。事实上:

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

最后,this.prop = ... 重新定义了刚刚删除的 属性。是defineProperty的俄罗斯套娃。

这是所有完全不必要的显式定义的样子:

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
    },
})

奖励回合

有趣的事实:在 JS 中,将 undefined 分配给我们想要“删除”(或假装它根本不存在)的对象 属性 是一种常见的模式。甚至有一种新的语法可以帮助解决这个问题,称为 "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

但是,我们仍然可以“检测”到 属性 在分配 undefined 时存在。只有delete才能真正从对象中移除:

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 的回答让我有了以下理解。希望@sheraff 的回答的这种重新措辞对其他人有用。

来自 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');
},

并未展示 getter/setter 的记忆行为,而是在使用 getters/setters 时实现记忆。

如文章所述:

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

下面的示例代码片段帮助我认识到了这一点。

这是一个基于文章中提供的示例代码的工作片段:

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

这是一个更详细的代码版本,它简化了一些 JavaScript 技巧,因此我更容易理解(@sheraff 的回答帮助我获得了对这段代码的评论):

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

如果我从未读过这篇文章并且来自另一种编程语言,这就是我实现记忆的方式:

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

可能存在效率低下和其他我不知道的原因应该阻止以这种方式实现记忆,但我理解它简单直接。这是我期望在实现记忆化的文章中看到的内容。没看到,我不知何故以为这篇文章只是在演示记忆。

以上所有代码片段都实现了记忆化,这就是文章中示例代码的意图。我最初的困惑是误读代码正在演示记忆,这使得 delete 语句对我来说似乎很奇怪。

希望对您有所帮助