如何在自定义 Object.prototype.xxx 函数中获取对象本身?

How to get object itself in custom Object.prototype.xxx function?

Object.prototype.getB = function() {

  // how to get the current value a
  return a.b;

};
const a = {b: 'c'};
a.getB();

如您所见,我想为所有对象值创建一个函数。我需要在这个函数中获取对象值然后做一些事情。

猴子补丁

你想做的事情叫做monkey patching —— 你改变了一个内置的原型。 这样做有很多错误的方法,应该完全避免,因为它会对 Web 兼容性产生负面影响。 但是,我将演示如何以最接近现有原型功能的方式完成此操作。

在您的例子中,函数体应该 return this.b。 在称为方法的函数中,您可以使用 this 关键字获取对象本身。 有关详细信息,请参阅 How does the "this" keyword work?(第 4 节:“输入功能代码”,第 “功能属性” 小节。

您正确地将方法添加到 Object.prototype。 有关详细信息,请参阅 Inheritance and the prototype chain

工具

推理猴子补丁时涉及到许多工具:

  1. 正在检查own property是否存在
  2. 使用property descriptors
  3. 使用正确的函数种类
  4. Getters and setters

假设您要在 TheClass 上实施 theMethod

1。检查自己 属性 是否存在

根据您的用例,您可能需要检查您要引入的方法是否已经存在。 你可以用 Object.hasOwn;这是一种相当新的方法,但在较旧的环境中,它可以简单地替换为 Object.prototype.hasOwnProperty.call。 或者,正常使用 hasOwnProperty,但请注意,如果您或其他人对 hasOwnProperty 方法 本身 进行猴子修补,这可能会导致不正确的结果。

请注意,in 不会专门检查自己的属性,但也会检查继承的属性,这不是(必然)您要创建 own 属性 对象。 另外,请注意 if(TheClass.prototype.theMethod) 不是 属性 存在性检查;这是一张 truthiness 支票。

代码示例
if(Object.hasOwn(TheClass.prototype, "theMethod")){
  // Define the method.
}
if(Object.prototype.hasOwnProperty.call(TheClass.prototype, "theMethod")){
  // Define the method.
}
if(TheClass.prototype.hasOwnProperty("theMethod")){
  // Define the method.
}

2。使用 属性 个描述符

您可以随意选择 属性 描述符,但是 现有的 方法是 可写、可配置和不可枚举的 (最后一个是使用 defineProperty 时的默认值)。 defineProperties 可用于一次性定义多个属性。

当使用 = 简单地分配一个 属性 时,属性 变得可写、可配置,并且 可枚举

代码示例
// Define the method:
Object.defineProperty(TheClass.prototype, "theMethod", {
  writable: true,
  configurable: true,
  value: function(){}
});
// Define the method:
Object.defineProperties(TheClass.prototype, {
  theMethod: {
    writable: true,
    configurable: true,
    value: function(){}
  }
});

3。使用正确的函数 kind

JavaScript 有四种主要的功能,它们有不同的用例。 “Is invokable”意味着它可以被调用 without new 而“Is constructable”意味着它可以被调用 with new.

Function kind Example Is invokable Is constructable Has this binding
Arrow function () => {} Yes No No
Method ({ method(){} }).method Yes No Yes
Class (class{}) No Yes Yes
function function (function(){}) Yes Yes Yes

我们正在寻找的是一种可以调用(无需 new)并具有自己的 this 绑定的方法。 我们可以使用 functions,但是,看看 现有的 方法,不仅 new "HELLO".charAt(); 很奇怪,而且也行不通! 所以该方法也不应该是可构造的。 因此,正确的方法就是我们要找的。

请注意,这显然取决于您的用例。 例如,如果您想要一个可构造的函数,请务必使用 class

代码示例

我们回到前面的代码示例,而不是 function(){} 使用方法定义。

// Define the method:
Object.defineProperty(TheClass.prototype, "theMethod", {
  writable: true,
  configurable: true,
  value: {
    theMethod(){
      // Do the thing.
    }
  }.theMethod
});

为什么要考虑 2. 和 3.?

考虑可枚举性和可构造性的目标是创建与现有内置方法具有相同“外观和感觉”的东西。 可以使用以下代码片段演示它们与简单实现之间的区别:

class TheClass{}

TheClass.prototype.theMethod = function(){};

让我们将其与另一种 内置 方法进行比较,例如 String.prototype.charAt:

Code snippet Naive example Built-in example
thePrototype Is TheClass.prototype Is String.prototype
theMethod Is TheClass.prototype.theMethod Is String.prototype.charAt
for(const p in thePrototype){ console.log(p); } "theMethod" will be logged at some point. "charAt" will never be logged.
new theMethod Creates an instance of theMethod. TypeError: theMethod is not a constructor.

使用第 2 和第 3 小节中的工具可以创建行为更像内置方法的方法。

4。吸气剂和 setters

另一种方法是使用 getter。考虑一下:

const arr = [
    "a",
    "b",
    "c",
  ];

console.log(arr.indexOfB); // 1

Array 原型上的 indexOfB getter 会是什么样子? 不能用上面的方法把value换成get,否则会得到:

TypeError: property descriptors must not specify a value or be writable when a getter or setter has been specified

属性 writable 需要从描述符中完全删除。 现在 value 可以替换为 get:

Object.defineProperty(Array.prototype, "indexOfB", {
  configurable: true,
  get: {
    indexOfB(){
      return this.indexOf("b");
    }
  }.indexOfB
});

A setter 也可以通过在描述符中添加一个 set 属性 来指定:

Object.defineProperty(Array.prototype, "indexOfB", {
  configurable: true,
  get: {
    indexOfB(){
      return this.indexOf("b");
    }
  }.indexOfB,
  set: {
    indexOfB(newValue){
      // `newValue` is the assigned value.
      // Use `this` for the current Array instance.
      // No `return` necessary.
    }
  }.indexOfB
});

Web 兼容性影响

有人想要扩展内置原型的原因有几个:

  • 您自己有了一个绝妙的主意,可以将新功能添加到您正在扩展的 class 的所有实例中,或者
  • 您想将现有的指定功能向后移植到旧版浏览器。

如果扩展目标对象,有两个选项需要考虑:

  • 如果 属性 不存在,则提供它,否则,保留现有的 属性,或者
  • 始终将 属性 替换为您自己的实现,无论它是否存在。

所有方法大多都有缺点:

如果您或其他人发明了自己的功能,但出现了同名的标准方法,那么这些功能几乎可以肯定是不兼容的。

如果您或其他人尝试实现标准功能以便将其向后移植到不支持它的浏览器,但不阅读规范并“猜测”它是如何工作的,那么 polyfilled 功能就是几乎可以保证与标准实现不兼容。 即使严格遵守规范,谁来确保跟上规范的变化? 谁来负责实施中可能出现的错误?

如果您或其他人选择在覆盖之前检查该功能是否存在,那么一旦使用支持该功能的浏览器的人访问您的页面,突然 一切都坏了,因为实现结果不兼容。

如果您或其他人无论如何都选择覆盖该功能,那么至少实现是一致的,但迁移到标准功能可能会很困难。 如果您编写的库在其他软件中使用很多,那么迁移成本会变得非常大,以至于标准本身必须改变;这就是为什么 Array.prototype.contains 必须重命名为 Array.prototype.includes[Reddit] [ESDiscuss] [Bugzilla] [MooTools], and Array.prototype.flatten could not be used and had to be named Array.prototype.flat instead[Pull request 1] [Pull request 2] [Bugzilla] 的原因。 换句话说,这就是我们不能拥有美好事物的原因

此外,您的库可能无法与其他库互操作。

备选方案

最简单的替代方法是定义您自己的纯函数:

const theMethod = (object) => object.theProperty; // Or whatever.
const theProperty = theMethod(theObject);

你也可以考虑Proxy。 这样你就可以动态查询 属性 并响应它。 假设你有一个属性为 az 的对象,你想实现方法 getAgetZ:

const theProxiedObject = new Proxy(theObject, {
    get(target, property, receiver){
      const letter = property.match(/^get(?<letter>[A-Z])$/)?.groups?.letter.toLowerCase();
      
      if(letter){
        return () => target[letter];
      }
      
      return Reflect.get(target, property, receiver);
    }
  });

console.assert(theProxiedObject.getB() === theProxiedObject.b);

您还可以使用另一个 class 扩展对象的原型,并改用这个:

class GetterOfB extends Object{
  b;
  constructor(init){
    super();
    Object.assign(this, init);
  }
  getB(){
    return this.b;
  }
}

const theObject = new GetterOfB({
    b: "c"
  });

const theB = theObject.getB();

你要记住的是不要修改你没有定义的东西。