Как мне написать метод расширения на JavaScript?

85

Мне нужно написать несколько методов расширения на JS. Я знаю, как это сделать на C #. Пример:

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

а затем позвонил:

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

Как мне сделать что-то подобное в JavaScript?

Мэтт Кэшатт
источник

Ответы:

156

JavaScript не имеет точного аналога для методов расширения C #. JavaScript и C # - совершенно разные языки.

Ближайшая подобная вещь, чтобы изменить объект - прототип всех строковых объектов: String.prototype. В общем, лучшая практика - не изменять прототипы встроенных объектов в коде библиотеки, предназначенные для объединения с другим кодом, который вы не контролируете. (Сделать это в приложении, где вы контролируете, какой еще код включается в приложение, нормально.)

Если вы делаете изменения прототипа встроенной в, это лучше всего (на сегодняшний день) , чтобы сделать это в несчетное свойство, используя Object.defineProperty(ES5 +, поэтому в основном любую современную среду JavaScript, а не IE8¹ или ранее). Чтобы соответствовать перечисляемости, возможности записи и настройки других строковых методов, это будет выглядеть так:

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.prototypeили Object.prototype; создание на них перечислимых свойств - Плохая вещь.

Детали:

JavaScript - это прототип языка. Это означает, что каждый объект поддерживается объектом-прототипом . В JavaScript этот прототип назначается одним из четырех способов:

  • С помощью функции-конструктора для объекта (например, new Fooсоздает объект в Foo.prototypeкачестве прототипа)
  • С помощью Object.createфункции, добавленной в ES5 (2009 г.)
  • По __proto__свойству аксессуара (ES2015 +, только в веб-браузерах, существовало в некоторых средах до стандартизации) или Object.setPrototypeOf(ES2015 +)
  • Механизмом JavaScript при создании объекта для примитива, потому что вы вызываете для него метод (это иногда называется «продвижение»).

Итак, в вашем примере, поскольку firstNameэто строковый примитив, он становится Stringэкземпляром всякий раз, когда вы вызываете для него метод, а Stringпрототип этого экземпляра String.prototype. Итак, добавив свойство вString.prototype ссылкам на вашу SayHiфункцию делает эту функцию доступной для всех Stringэкземпляров (и, по сути, для строковых примитивов, потому что они продвигаются).

Пример:

Между этим и C # методами расширения есть несколько ключевых отличий:

  • (Как указал DougR в комментарии) Методы расширения C # можно вызывать по nullссылкам . Если у вас есть stringметод расширения, этот код:

    string s = null;
    s.YourExtensionMethod();
    

    работает. Это не так с JavaScript; nullявляется его собственным типом, и любая ссылка на свойство nullвызывает ошибку. (И даже если бы этого не произошло, прототипа для расширения типа Null не существует.)

  • (Как отметил ChrisW в комментарии) Методы расширения C # не являются глобальными. Они доступны, только если пространство имен, в котором они определены, используется кодом с помощью метода расширения. (Они на самом деле являются синтаксическим сахаром для статических вызовов, поэтому они работают null.) Это неверно в JavaScript: если вы измените прототип встроенного модуля, это изменение будет замечено всем кодом во всей области, в которой вы находитесь. сделайте это в (область - это глобальная среда и связанные с ней внутренние объекты и т. д.). Поэтому, если вы сделаете это на веб-странице, весь код, который вы загружаете на этой странице, увидит изменение. Если вы сделаете это в модуле Node.js, всекод, загруженный в той же области, что и этот модуль, увидит изменение. В обоих случаях, поэтому вы не делаете этого в коде библиотеки. (Веб-воркеры и рабочие потоки Node.js загружаются в свою собственную область, поэтому у них другая глобальная среда и другие встроенные функции, чем у основного потока. Но эта область по-прежнему используется всеми модулями, которые они загружают.)


¹ IE8 есть Object.defineProperty, но он работает только с объектами DOM, а не с объектами JavaScript. String.prototypeэто объект JavaScript.

TJ Crowder
источник
@DougR: Извините, стандартная очистка комментариев. Когда комментарий устаревает, модники или опытные пользователи могут удалить его (моды могут делать это напрямую, опытные пользователи должны объединиться в очередь на проверку). Я обновил ответ, чтобы указать, что вы отметили это. :-)
TJ Crowder
1
@Grundy: Спасибо, да, весь смысл этой String s = null;части был в использовании s.YourExtensionMethod()! Оцените улов. :-)
TJ Crowder
Спасибо, что подчеркнули, что метод расширения действительно работает для нулевых сущностей.
Ram
1
Если вы сделаете это, например, в Node.js, я думаю, это повлияет (добавит новое свойство) на каждую строку в программе, включая другие модули? Конфликтуют ли два модуля, если они оба определяют свойство SayHi?
ChrisW
1
@ChrisW - Вполне. :-) Вместо этого вы можете создать подкласс Array ( class MyArray extends Array { singleOrDefault() { ... }}), но это означает, что вы должны сделать это MyArray.from(originalArray)перед его использованием, если originalArrayэто утомительный старый массив. Существуют также LINQ-подобные библиотеки для JavaScript ( вот обзор ), которые аналогичным образом включают создание объекта 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 docs или MDN
user1306322