Як написати метод розширення в JavaScript?


84

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

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

а потім зателефонував:

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

Як я можу зробити щось подібне в JavaScript?

Відповіді:


150

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

(За замовчуванням для enumerableIS 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__властивістю accessor (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.


@DougR: Вибачте, стандартне очищення коментарів. Коли коментар застаріває, моди або досвідчені користувачі можуть його видалити (моди можуть це зробити безпосередньо, досвідчені користувачі повинні об'єднатися в черзі огляду). Я оновив відповідь, щоб повідомити, що Ви позначили це. :-)
TJ Crowder

1
@Grundy: Дякую, так, вся суть цієї String s = null;частини полягала у використанні s.YourExtensionMethod()! Оцініть улов. :-)
TJ Crowder

Дякуємо, що підкреслили, що метод розширення працює для нульових сутностей.
Рам,

1
Наприклад, якщо ви зробите це в Node.js, я думаю, це вплине (додасть нову властивість) на кожен рядок у програмі - включаючи інші модулі? Чи конфліктують два модулі, якщо обидва вони визначають властивість "SayHi"?
ChrisW

1
@ChrisW - Цілком. :-) Що б ви могли зробити замість цього, це створити підклас Array ( class MyArray extends Array { singleOrDefault() { ... }}), але це означає, що вам потрібно зробити це MyArray.from(originalArray)перед використанням, якщо originalArrayце нудний старий масив. Існують також LINQ-подібні бібліотеки для JavaScript ( ось огляд ), які аналогічно передбачають створення об’єкта Linq з масиву перед тим, як робити щось (ну, LINQ до JavaScript робив, я не використовував інші [і не використовував цього у роках]). (BTW, оновлена ​​відповідь, thx.)
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, чи це щось, для чого мені потрібно було б зрозуміти, як використовувати машинопис? Я запитую, оскільки ви посилаєтесь на typecriptlang.org і не говорите ES6 docs або MDN
user1306322
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.