如何在 JavaScript 中编写扩展方法?

IT技术 javascript extension-methods
2021-02-03 15:21:29

我需要在JS中编写一些扩展方法。我知道如何在 C# 中做到这一点。例子:

public static string SayHi(this Object name)
{
    return "Hi " + name + "!";
}

然后通过以下方式调用:

string firstName = "Bob";
string hi = firstName.SayHi();

我将如何在 JavaScript 中做这样的事情?

2个回答

JavaScript 没有与 C# 的扩展方法完全类似的东西。JavaScript 和 C# 是完全不同的语言。

最接近类似的就是修改所有字符串对象的原型对象:String.prototype. 通常,最佳实践是不要修改库代码中旨在与您无法控制的其他代码组合的内置对象的原型。(在您控制应用程序中包含哪些其他代码的应用程序中执行此操作是可以的。)

如果您确实修改了内置的原型,最好(到目前为止)通过使用(ES5+,所以基本上是任何现代 JavaScript 环境,而不是 IE8¹ 或更早版本)使其成为不可枚举的属性Object.defineProperty为了匹配其他字符串方法的可枚举性、可写性和可配置性,它看起来像这样:

Object.defineProperty(String.prototype, "SayHi", {
    value: function SayHi() {
        return "Hi " + this + "!";
    },
    writable: true,
    configurable: true
});

(默认为enumerableIS false)。

如果您需要支持过时的环境,那么 for String.prototype,特别是,您可能可以通过创建可枚举属性而逃脱:

// Don't do this if you can use `Object.defineProperty`
String.prototype.SayHi = function SayHi() {
    return "Hi " + this + "!";
};

这不是一个好主意,但你可能会侥幸逃脱。永远不要用Array.prototype那样做Object.prototype在这些上创建可枚举的属性是一件坏事™。

细节:

JavaScript 是一种原型语言。这意味着每个对象都由原型对象支持在 JavaScript 中,该原型以四种方式之一分配:

  • 通过对象构造函数(例如,new Foo创建一个对象Foo.prototype作为其原型)
  • 通过Object.createES5 (2009) 中添加功能
  • 通过__proto__访问器属性(ES2015+,仅在 Web 浏览器上,在标准化之前存在于某些环境中)或Object.setPrototypeOf(ES2015+)
  • 在为原始对象创建对象时由 JavaScript 引擎调用,因为您正在调用它的方法(这有时称为“提升”)

因此,在您的示例中,由于firstName是字符串原语,因此String每当您对其调用方法时,它都会被提升为实例,并且该String实例的原型是String.prototype. 因此,向String.prototype引用您的SayHi函数的属性添加一个属性可以使该函数在所有String实例上可用(并且在字符串基元上有效,因为它们得到了提升)。

例子:

此方法与 C# 扩展方法之间存在一些主要区别:

  • (正如DougR在评论中指出的那样) C# 的扩展方法可以在null引用上调用如果您有string扩展方法,则此代码:

      string s = null;
      s.YourExtensionMethod();
    

工作(除非YourExtensionMethodnull作为实例参数接收时抛出)。JavaScript 不是这样。null是它自己的类型,任何属性引用都会null引发错误。(即使没有,也没有原型可以扩展为 Null 类型。)

  • (正如ChrisW在评论中指出的那样) C# 的扩展方法不是全局的。只有当代码使用扩展方法使用它们定义的命名空间时,它们才可访问。(他们真的语法糖静态调用,这就是为什么他们的工作对null)那不是真的在JavaScript:如果您更改的原型内置,该变化是由看到所有在整个领域的代码,你这样做(领域是全球环境及其相关的内在对象等)。因此,如果您在网页中执行此操作,您在该页面上加载的所有代码都会看到更改。如果您在 Node.js module中执行此操作,则所有在与该module相同的领域中加载的代码将看到更改。在这两种情况下,这就是您不在库代码中执行此操作的原因。(Web 工作线程和 Node.js 工作线程加载在它们自己的领域中,因此它们具有与主线程不同的全局环境和不同的内在函数。但该领域仍然与它们加载的任何module共享。)

¹ IE8 确实有Object.defineProperty,但它只适用于 DOM 对象,而不适用于 JavaScript 对象。String.prototype是一个 JavaScript 对象。

@Grundy:谢谢,是的,这String s = null;部分的重点是使用s.YourExtensionMethod()欣赏收获。:-)
2021-03-15 15:21:29
当我了解到可以安全地在空实例上调用 C# 扩展方法时,我已经有好几岁了。谢谢!
2021-03-30 15:21:29
例如,如果您在 Node.js 中执行此操作,我想这会影响(将新属性添加到)程序中的每个字符串——包括其他module?如果两个module都定义了“SayHi”属性,它们会冲突吗?
2021-04-01 15:21:29
@ChrisW - 相当。:-) 你可能会做的是创建一个 Array 子类 ( class MyArray extends Array { singleOrDefault() { ... }}) 但这意味着MyArray.from(originalArray)如果originalArray是一个无聊的旧数组,你必须在使用它之前还有一些类似于 LINQ 的 JavaScript 库(这里是综述),它们同样涉及在做事情之前从数组创建一个 Linq 对象(嗯,LINQ to JavaScript 做了,我没有使用过其他的 [也没有使用过那个)多年])。(顺便说一句,更新了答案,谢谢。)
2021-04-13 15:21:29
@DougR:抱歉,标准评论清理。当评论过时时,模组或高级用户可以将其删除(模组可以直接删除,高级用户必须在评论队列中组队)。我已经更新了答案以指出您已将其标记出来。:-)
2021-04-14 15:21:29

使用全局增强

declare global {
    interface DOMRect {
        contains(x:number,y:number): boolean;
    }
}
/* Adds contain method to class DOMRect */
DOMRect.prototype.contains = function (x:number, y:number) {                    
    if (x >= this.left && x <= this.right &&
        y >= this.top && y <= this.bottom) {
        return true
    }
    return false
};
这是否适用于纯/vanilla javascript,还是我必须弄清楚如何使用typescript?我问的是因为你链接到 typescriptlang.org 而不是说 ES6 文档或 MDN
2021-04-14 15:21:29