Jak napisać metodę rozszerzenia w JavaScript?

85

Muszę napisać kilka metod rozszerzających w JS. Wiem, jak to zrobić w C #. Przykład:

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

a następnie wezwany przez:

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

Jak zrobiłbym coś takiego w JavaScript?

Matt Cashatt
źródło

Odpowiedzi:

155

JavaScript nie ma dokładnego odpowiednika metod rozszerzających języka C #. JavaScript i C # to całkiem różne języki.

Najbliższa podobna sprawa jest zmodyfikowanie obiektu prototypu wszystkich obiektów String: String.prototype. Ogólnie rzecz biorąc, najlepszą praktyką jest nie modyfikowanie prototypów obiektów wbudowanych w kodzie biblioteki, które mają być łączone z innym kodem, nad którym nie masz kontroli. (Zrobienie tego w aplikacji, w której kontrolujesz, jaki inny kod jest zawarty w aplikacji, jest w porządku).

Jeśli zrobić zmodyfikować prototyp wbudowany, najlepiej (jak dotąd), aby to się nie enumerable własność za pomocą Object.defineProperty(ES5 +, więc w zasadzie każdego nowoczesnego środowiska JavaScript, a nie IE8¹ lub wcześniej). Aby dopasować wyliczalność, zapisywalność i konfigurowalność innych metod łańcuchowych, wyglądałoby to następująco:

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

(Wartość domyślna enumerableto false.)

Jeśli potrzebujesz obsługiwać przestarzałe środowiska, to w String.prototypeszczególności prawdopodobnie możesz uciec od utworzenia wyliczalnej właściwości:

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

To nie jest dobry pomysł, ale może ci się to udać. Nigdy nie rób tego z Array.prototypelub Object.prototype; tworzenie na nich niezliczonych właściwości jest złą rzeczą ™.

Detale:

JavaScript to język prototypowy. Oznacza to, że każdy obiekt jest obsługiwany przez obiekt prototypowy . W JavaScript ten prototyp jest przypisywany na jeden z czterech sposobów:

  • Przez funkcję konstruktora dla obiektu (np. new FooTworzy obiekt z Foo.prototypeprototypem)
  • Według Object.createfunkcji dodanej w ES5 (2009)
  • Według __proto__właściwości accessor (ES2015 +, tylko w przeglądarkach internetowych, istniała w niektórych środowiskach, zanim została ujednolicona) lub Object.setPrototypeOf(ES2015 +)
  • Przez silnik JavaScript podczas tworzenia obiektu dla prymitywu, ponieważ wywołujesz na nim metodę (czasami nazywa się to „promocją”)

Więc w twoim przykładzie, ponieważ firstNamejest to ciąg znaków, jest promowany do Stringinstancji za każdym razem, gdy wywołujesz na niej metodę, a Stringjej prototypem jest String.prototype. Zatem dodanie właściwości do String.prototypetej SayHifunkcji, która odwołuje się do funkcji, sprawia, że ​​ta funkcja jest dostępna we wszystkich Stringinstancjach (i efektywnie na elementach łańcuchowych, ponieważ są promowane).

Przykład:

Istnieją pewne kluczowe różnice między this i metodami rozszerzenia języka C #:

  • (Jak zauważył DougR w komentarzu) Metody rozszerzające języka C # można wywoływać w nullodwołaniach . Jeśli masz stringmetodę rozszerzenia, ten kod:

    string s = null;
    s.YourExtensionMethod();
    

    Pracuje. To nie jest prawdą w przypadku JavaScript; nulljest własnym typem, a każde odwołanie do właściwości w przypadku nullzgłasza błąd. (A nawet gdyby tak się nie stało, nie ma prototypu do rozszerzenia dla typu Null).

  • (Jak wskazał ChrisW w komentarzu) Metody rozszerzające języka C # nie są globalne. Są dostępne tylko wtedy, gdy przestrzeń nazw, w której są zdefiniowane, jest używana przez kod przy użyciu metody rozszerzenia. (Są tak naprawdę cukrem syntaktycznym dla wywołań statycznych, dlatego też nad nimi pracują null.) To nieprawda w JavaScript: jeśli zmienisz prototyp elementu wbudowanego, ta zmiana będzie widoczna dla całego kodu w całej dziedzinie, zrób to w (sfera to globalne środowisko i związane z nim wewnętrzne obiekty itp.). Więc jeśli zrobisz to na stronie internetowej, cały kod, który załadujesz na tej stronie, zobaczy zmianę. Jeśli zrobisz to w module Node.js, wszystkie plikikod załadowany w tej samej dziedzinie, co ten moduł, zobaczy zmianę. W obu przypadkach dlatego nie robisz tego w kodzie biblioteki. (Pracownicy sieci i node.js wątków roboczych są ładowane we własnym królestwie, więc mają różne środowiska i różne intrinsics globalny niż głównego wątku. Ale królestwo jest nadal udostępniane żadnym modułów one obciążeniu.)


¹ IE8 tak jest Object.defineProperty, ale działa tylko na obiektach DOM, a nie na obiektach JavaScript. String.prototypejest obiektem JavaScript.

TJ Crowder
źródło
@DougR: Przepraszamy, standardowe czyszczenie komentarzy. Kiedy komentarz staje się nieaktualny, modyfikatorzy lub zaawansowani użytkownicy mogą go usunąć (modyfikatorzy mogą to zrobić bezpośrednio, użytkownicy zaawansowani muszą połączyć siły w kolejce recenzji). Zaktualizowałem odpowiedź, aby poinformować, że zgłosiłeś to. :-)
TJ Crowder
1
@Grundy: Dzięki, tak, celem tej String s = null;części było użycie s.YourExtensionMethod()! Doceń haczyk. :-)
TJ Crowder
Dziękujemy za podkreślenie, że metoda rozszerzenia działa dla encji null.
Ram
1
Jeśli zrobisz to na przykład w Node.js, myślę, że wpłynie to (doda nową właściwość) na każdy ciąg w programie - w tym na inne moduły? Czy dwa moduły byłyby w konflikcie, gdyby oba definiowały właściwość „SayHi”?
ChrisW
1
@ChrisW - Całkiem. :-) Zamiast tego możesz utworzyć podklasę Array ( class MyArray extends Array { singleOrDefault() { ... }}), ale oznacza to, że musisz to zrobić MyArray.from(originalArray)przed jej użyciem, jeśli originalArrayjest to nudna stara tablica. Istnieją również biblioteki podobne do LINQ dla JavaScript ( tutaj jest podsumowanie ), które podobnie obejmują utworzenie obiektu Linq z tablicy przed wykonaniem czynności (cóż, LINQ to JavaScript zrobił, nie używałem innych [i nie użyłem tego w latach]). (BTW, zaktualizowałem odpowiedź, dzięki.)
TJ Crowder,
0

Korzystanie z globalnego wzmocnienia

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
};
Saleh
źródło
czy to działa z czystym / waniliowym javascriptem, czy jest to coś, do czego muszę się dowiedzieć, jak używać maszynopisu? Pytam, ponieważ podajesz link do typescriptlang.org i nie mówisz o dokumentach ES6 lub MDN
user1306322