如何用JavaScript编写扩展方法?


83

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

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

然后由以下人员调用:

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

我将如何在JavaScript中执行类似的操作?

Answers:


149

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

(默认enumerable值为false。)

如果您需要支持过时的环境,那么特别是对于String.prototype,您可能可以创建一个可枚举的属性:

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

这不是一个好主意,但是您可能会摆脱它。从来没有做到这一点与Array.prototypeObject.prototype; 在这些东西上创建可枚举的属性是Bad Thing™。

细节:

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

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

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

例:

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

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

    string s = null;
    s.YourExtensionMethod();
    

    作品。JavaScript并非如此;null是它自己的类型,任何属性引用都会null引发错误。(即使没有,也没有原型可以扩展为Null类型。)

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


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


@DougR:对不起,标准注释清除。当评论过时时,mod或高级用户可以将其删除(mod可以直接进行评论,高级用户必须在审核队列中组队)。我已经更新了答案,以提醒您标记了此内容。:-)
TJ Crowder 2015年

1
@Grundy:谢谢,是的,这String s = null;部分的全部要用s.YourExtensionMethod()!欣赏收获。:-)
TJ Crowder 2015年

感谢您强调指出扩展方法确实适用于空实体。
2016年

1
例如,如果您在Node.js中执行此操作,我想这会影响程序中的每个字符串(包括其他模块)(向其中添加新属性)?如果两个模块都定义了“ SayHi”属性,是否会发生冲突?
ChrisW '18

1
@ChrisW-非常。:-)您可能要做的是创建一个Array子类(class MyArray extends Array { singleOrDefault() { ... }}),但这意味着MyArray.from(originalArray)如果originalArray是一个无聊的旧数组,则必须在使用它之前先做。还有一些针对JavaScript的类似LINQ的库(这是一个综述),类似地,它涉及在执行操作之前从数组创建Linq对象(嗯,LINQ to JavaScript就是这样做的,我没有使用过其他工具[也没有使用过那个多年])。(顺便说一句,更新了答案,谢谢。)
TJ Crowder

0

使用全局扩充

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
};

这可用于纯/原始javascript还是我必须弄清楚如何使用打字稿的东西?我问,因为您链接到typescriptlang.org,而不是说ES6文档或MDN
user1306322
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.