Dlaczego rozszerzanie obiektów natywnych jest złą praktyką?

140

Każdy lider opinii JS mówi, że rozszerzanie obiektów natywnych jest złą praktyką. Ale dlaczego? Czy dostaniemy hit wydajności? Czy boją się, że ktoś zrobi to „w niewłaściwy sposób” i dodaje do tego niezliczone typy Object, praktycznie niszcząc wszystkie pętle na dowolnym obiekcie?

Weź TJ Holowaychuk „s should.js na przykład. On dodaje prosty getter do Objecti wszystko działa poprawnie ( źródło ).

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(Object(this).valueOf());
  },
  configurable: true
});

To naprawdę ma sens. Na przykład można by przedłużyć Array.

Array.defineProperty(Array.prototype, "remove", {
  set: function(){},
  get: function(){
    return removeArrayElement.bind(this);
  }
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);

Czy są jakieś argumenty przeciwko rozszerzaniu typów natywnych?

buschtoens
źródło
9
Czego spodziewasz się, gdy później obiekt natywny zostanie zmieniony tak, aby zawierał funkcję „usuń” z inną semantyką niż Twoja własna? Nie kontrolujesz standardu.
ta.speot.is
1
To nie jest twój rodzimy typ. To rodzimy typ każdego.
7
„Czy boją się, że ktoś zrobi to„ w niewłaściwy sposób ”i dodaje do Object niezliczone typy, praktycznie niszcząc wszystkie pętle na dowolnym obiekcie? : Tak. W czasach, gdy formułowano tę opinię, niemożliwe było stworzenie niewliczalnych właściwości. Teraz może być inaczej pod tym względem, ale wyobraź sobie, że każda biblioteka po prostu rozszerza natywne obiekty tak, jak chcą. Jest powód, dla którego zaczęliśmy używać przestrzeni nazw.
Felix Kling
5
Zresztą niektórzy „liderzy opinii”, na przykład Brendan Eich, uważają, że rozszerzenie rodzimych prototypów jest w porządku.
Benjamin Gruenbaum
2
W dzisiejszych czasach Foo nie powinno być globalne, mamy include, requirejs, commonjs itp.
Jamie Pate

Odpowiedzi:

132

Rozszerzając obiekt, zmieniasz jego zachowanie.

Zmiana zachowania obiektu, który będzie używany tylko przez Twój własny kod, jest w porządku. Ale kiedy zmienisz zachowanie czegoś, co jest również używane przez inny kod, istnieje ryzyko, że złamiesz ten inny kod.

Jeśli chodzi o dodawanie metod do klas obiektów i tablic w javascript, ryzyko zepsucia czegoś jest bardzo wysokie ze względu na sposób działania javascript. Wieloletnie doświadczenie nauczyło mnie, że tego rodzaju rzeczy powodują różnego rodzaju okropne błędy w javascript.

Jeśli potrzebujesz niestandardowego zachowania, znacznie lepiej jest zdefiniować własną klasę (być może podklasę) zamiast zmieniać natywną. W ten sposób niczego nie zepsujesz.

Możliwość zmiany sposobu działania klasy bez tworzenia podklas jest ważną cechą każdego dobrego języka programowania, ale należy jej używać rzadko i ostrożnie.

Abhi Beckert
źródło
2
Więc dodanie czegoś takiego .stgringify()byłoby uważane za bezpieczne?
buschtoens,
7
Jest wiele problemów, na przykład co się stanie, jeśli inny kod również spróbuje dodać własną stringify()metodę z innym zachowaniem? To naprawdę nie jest coś, co powinieneś robić w codziennym programowaniu ... nie, jeśli wszystko, co chcesz zrobić, to zapisać kilka znaków kodu tu i tam. Lepiej jest zdefiniować własną klasę lub funkcję, która akceptuje dane wejściowe i określa je. Większość przeglądarek definiuje JSON.stringify(), najlepiej byłoby sprawdzić, czy to istnieje, a jeśli nie zdefiniować tego samodzielnie.
Abhi Beckert,
5
Wolę someError.stringify()więcej errors.stringify(someError). Jest prosty i doskonale pasuje do koncepcji js. Robię coś, co jest ściśle powiązane z określonym obiektem ErrorObject.
buschtoens
1
Jedynym (ale dobrym) argumentem, który pozostaje, jest to, że niektóre ogromne biblioteki makr mogą zacząć przejmować twoje typy i mogą wzajemnie się zakłócać. Czy jest coś jeszcze?
buschtoens
4
Jeśli problem polega na tym, że możesz mieć kolizję, czy możesz użyć wzorca, w którym za każdym razem, gdy to robisz, zawsze potwierdzasz, że funkcja jest niezdefiniowana, zanim ją zdefiniujesz? A jeśli zawsze umieszczasz biblioteki stron trzecich przed własnym kodem, powinieneś być w stanie zawsze wykryć takie kolizje.
Dave Cousineau
32

Nie ma wymiernej wady, takiej jak hit wydajnościowy. Przynajmniej nikt o tym nie wspomniał. Jest to więc kwestia osobistych preferencji i doświadczeń.

Główny argument za: wygląda lepiej i jest bardziej intuicyjny: cukier składniowy. Jest to funkcja specyficzna dla typu / instancji, więc powinna być konkretnie powiązana z tym typem / instancją.

Główny argument przeciwny: kod może przeszkadzać. Jeśli biblioteka A dodaje funkcję, może nadpisać funkcję z biblioteki B. Może to bardzo łatwo złamać kod.

Obie mają rację. Kiedy polegasz na dwóch bibliotekach, które bezpośrednio zmieniają twoje typy, najprawdopodobniej skończysz z uszkodzonym kodem, ponieważ oczekiwana funkcjonalność prawdopodobnie nie jest taka sama. Całkowicie się z tym zgadzam. Biblioteki makr nie mogą manipulować typami natywnymi. W przeciwnym razie jako programista nigdy nie będziesz wiedział, co tak naprawdę dzieje się za kulisami.

I to jest powód, dla którego nie lubię bibliotek takich jak jQuery, underscore itp. Nie zrozum mnie źle; są absolutnie dobrze zaprogramowane i działają jak urok, ale są duże . Używasz tylko 10% z nich i rozumiesz około 1%.

Dlatego wolę podejście atomistyczne , w którym potrzebujesz tylko tego, czego naprawdę potrzebujesz. W ten sposób zawsze wiesz, co się stanie. Mikro-biblioteki robią tylko to, co chcesz, aby nie przeszkadzały. W kontekście wiedzy użytkownika końcowego, które funkcje są dodawane, rozszerzanie typów natywnych można uznać za bezpieczne.

TL; DR W razie wątpliwości nie rozszerzaj typów natywnych. Rozszerz typ natywny tylko wtedy, gdy masz 100% pewności, że użytkownik końcowy będzie wiedział o takim zachowaniu i będzie chciał. W żadnym wypadku nie manipuluj istniejącymi funkcjami typu natywnego, ponieważ spowodowałoby to uszkodzenie istniejącego interfejsu.

Jeśli zdecydujesz się rozszerzyć typ, użyj Object.defineProperty(obj, prop, desc); jeśli nie możesz , użyj tego typu prototype.


Pierwotnie wymyśliłem to pytanie, ponieważ chciałem, Erroraby można było wysłać je przez JSON. Potrzebowałem więc sposobu, aby je uszlachetnić. error.stringify()czuł się o wiele lepiej niż errorlib.stringify(error); jak sugeruje druga konstrukcja, działam na sobie, errorliba nie na errorsobie.

buschtoens
źródło
Jestem otwarty na dalsze opinie na ten temat.
buschtoens
4
Czy sugerujesz, że jQuery i underscore rozszerzają obiekty natywne? Oni nie. Więc jeśli unikasz ich z tego powodu, mylisz się.
JLRishe
1
Oto pytanie, które moim zdaniem jest pomijane w argumencie, że rozszerzanie obiektów natywnych o bibliotekę A może kolidować z biblioteką B: Dlaczego mielibyście mieć dwie biblioteki, które są albo tak podobne w naturze, albo mają tak szeroki charakter, że konflikt jest możliwy? Tzn. Albo wybieram lodash, albo podkreślam nie oba. Nasz obecny krajobraz bibliotek w javascript jest tak przesycony, a programiści (ogólnie) są tak nieostrożni w dodawaniu ich, że w końcu
unikamy
2
@micahblu - biblioteka mogłaby zdecydować się na modyfikację standardowego obiektu tylko dla wygody własnego wewnętrznego programowania (często dlatego ludzie chcą to robić). Tak więc tego konfliktu nie można uniknąć tylko dlatego, że nie używałbyś dwóch bibliotek, które mają tę samą funkcję.
jfriend00
1
Możesz również (od es2015) utworzyć nową klasę, która rozszerza istniejącą klasę i użyć jej we własnym kodzie. więc jeśli MyError extends Errormoże mieć stringifymetodę bez kolizji z innymi podklasami. Nadal musisz radzić sobie z błędami, które nie zostały wygenerowane przez Twój własny kod, więc może to być mniej przydatne Errorniż w przypadku innych.
ShadSterling
17

Moim zdaniem to zła praktyka. Głównym powodem jest integracja. Cytując dokumenty powinny.js:

OMG TO ROZSZERZA OBIEKT ???!?! @ Tak, tak, z jednym getter powinien i nie, nie złamie twojego kodu

Cóż, skąd autor może wiedzieć? A co, jeśli mój kpijący framework robi to samo? A jeśli moja obietnica lib zrobi to samo?

Jeśli robisz to we własnym projekcie, to jest w porządku. Ale dla biblioteki to zły projekt. Underscore.js to przykład tego, co zrobiono we właściwy sposób:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Stefan
źródło
2
Jestem pewien, że odpowiedź TJ byłoby nie korzystać z tych obietnic ani ram szydząc: X
Jim Schubert
_(arr).flatten()Przykład rzeczywiście przekonał mnie, aby nie przedłużać rodzimych obiektów. Mój osobisty powód, aby to zrobić, był czysto syntaktyczny. Ale to satysfakcjonuje moje odczucie estetyczne :) Nawet użycie bardziej zwykłej nazwy funkcji, takiej jak foo(native).coolStuff()przekształcenie jej w jakiś „rozszerzony” obiekt, wygląda świetnie syntaktycznie. Więc dzięki za to!
egst
17

Jeśli spojrzysz na to indywidualnie, być może niektóre implementacje są dopuszczalne.

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.

Nadpisywanie już utworzonych metod stwarza więcej problemów niż rozwiązuje, dlatego w wielu językach programowania powszechnie mówi się, aby unikać tej praktyki. Skąd deweloperzy mają wiedzieć, że funkcja została zmieniona?

String.prototype.capitalize = function capitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.

W tym przypadku nie nadpisujemy żadnej znanej podstawowej metody JS, ale rozszerzamy String. Jeden z argumentów w tym poście wspomniał, w jaki sposób nowy programista ma wiedzieć, czy ta metoda jest częścią podstawowego JS, lub gdzie znaleźć dokumenty? Co by się stało, gdyby podstawowy obiekt JS String miał otrzymać metodę o nazwie capitalize ?

Co jeśli zamiast dodawać nazwy, które mogą kolidować z innymi bibliotekami, użyłeś modyfikatora specyficznego dla firmy / aplikacji, który wszyscy programiści mogliby zrozumieć?

String.prototype.weCapitalize = function weCapitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.

var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.

Gdybyś nadal rozszerzał inne obiekty, wszyscy deweloperzy napotkaliby je na wolności z (w tym przypadku) my , które powiadomiłoby ich, że jest to rozszerzenie specyficzne dla firmy / aplikacji.

Nie eliminuje to kolizji nazw, ale ogranicza możliwość. Jeśli stwierdzisz, że rozszerzanie podstawowych obiektów JS jest dla Ciebie i / lub Twojego zespołu, być może jest to dla Ciebie.

SoEzPz
źródło
7

Rozszerzanie prototypów wbudowanych jest rzeczywiście złym pomysłem. Jednak ES2015 wprowadził nową technikę, którą można wykorzystać do uzyskania pożądanego zachowania:

Wykorzystanie WeakMaps do kojarzenia typów z wbudowanymi prototypami

Poniższa implementacja rozszerza prototypy Numberi Arraybez ich dotykania:

// new types

const AddMonoid = {
  empty: () => 0,
  concat: (x, y) => x + y,
};

const ArrayMonoid = {
  empty: () => [],
  concat: (acc, x) => acc.concat(x),
};

const ArrayFold = {
  reduce: xs => xs.reduce(
   type(xs[0]).monoid.concat,
   type(xs[0]).monoid.empty()
)};


// the WeakMap that associates types to prototpyes

types = new WeakMap();

types.set(Number.prototype, {
  monoid: AddMonoid
});

types.set(Array.prototype, {
  monoid: ArrayMonoid,
  fold: ArrayFold
});


// auxiliary helpers to apply functions of the extended prototypes

const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);


// mock data

xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];


// and run

console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);

Jak widać, nawet prymitywne prototypy można rozszerzyć tą techniką. Używa struktury mapy i Objecttożsamości do kojarzenia typów z wbudowanymi prototypami.

Mój przykład włącza reducefunkcję, która oczekuje tylko Arrayjako pojedynczego argumentu, ponieważ może wydobyć informacje, jak utworzyć pusty akumulator i jak połączyć elementy z tym akumulatorem z elementów samej tablicy.

Proszę zauważyć, że mogłem użyć normalnego Maptypu, ponieważ słabe odwołania nie mają sensu, gdy reprezentują tylko wbudowane prototypy, które nigdy nie są usuwane jako śmieci. Jednak WeakMapnie można go iterować i nie można go sprawdzić, chyba że masz odpowiedni klucz. Jest to pożądana funkcja, ponieważ chcę uniknąć jakiejkolwiek formy odbicia typu.


źródło
To fajne użycie WeakMap. Uważaj jednak xs[0]na puste tablice. type(undefined).monoid ...
Dziękuję
@naomik Wiem - zobacz moje ostatnie pytanie drugi fragment kodu.
Jak standardowe jest wsparcie dla tego @ftor?
onassar
5
-1; jaki jest sens tego wszystkiego? Po prostu tworzysz oddzielny słownik globalny mapujący typy na metody, a następnie jawnie wyszukujesz metody w tym słowniku według typu. W konsekwencji tracisz wsparcie dla dziedziczenia (metoda „dodana” do Object.prototypew ten sposób nie może być wywołana na an Array), a składnia jest znacznie dłuższa / brzydsza niż gdybyś rzeczywiście rozszerzył prototyp. Niemal zawsze byłoby prostsze tworzenie klas narzędziowych z metodami statycznymi; jedyną zaletą tego podejścia jest pewne ograniczone wsparcie dla polimorfizmu.
Mark Amery,
4
@MarkAmery Hej kolego, nie rozumiesz. Nie tracę Dziedzictwa, ale się go pozbywam. Dziedziczenie jest takie 80-te, że powinieneś o tym zapomnieć. Celem tego hacka jest naśladowanie klas typów, które po prostu są przeciążonymi funkcjami. Istnieje jednak uzasadniona krytyka: czy warto naśladować klasy typów w Javascript? Nie, nie jest. Zamiast tego użyj języka maszynowego, który obsługuje je natywnie. Jestem prawie pewien, że to miałeś na myśli.
7

Jeszcze jeden powód, dla którego nie powinieneś rozszerzać natywnych obiektów:

Używamy Magento, które używa prototype.js i rozszerza wiele rzeczy na natywne Obiekty. Działa to dobrze, dopóki nie zdecydujesz się na nowe funkcje i tam zaczynają się duże problemy.

Wprowadziliśmy Webcomponents na jednej z naszych stron, więc webcomponents-lite.js decyduje się zastąpić cały (natywny) obiekt zdarzenia w IE (dlaczego?). To oczywiście psuje prototype.js, co z kolei psuje Magento. (dopóki nie znajdziesz problemu, możesz poświęcić wiele godzin na jego śledzenie)

Jeśli lubisz kłopoty, rób to dalej!

Eugen Wesseloh
źródło
5

Widzę trzy powody, aby tego nie robić ( przynajmniej z poziomu aplikacji ), z których tylko dwa są omówione w istniejących odpowiedziach tutaj:

  1. Jeśli zrobisz to źle, przypadkowo dodasz wyliczalną właściwość do wszystkich obiektów typu rozszerzonego. Łatwo obejść użycie Object.defineProperty, które domyślnie tworzy niewliczalne właściwości.
  2. Możesz spowodować konflikt z biblioteką, której używasz. Można tego uniknąć z należytą starannością; po prostu sprawdź, jakie metody definiują używane przez Ciebie biblioteki przed dodaniem czegoś do prototypu, sprawdź informacje o wersji podczas aktualizacji i przetestuj swoją aplikację.
  3. Może to spowodować konflikt z przyszłą wersją natywnego środowiska JavaScript.

Punkt 3 jest prawdopodobnie najważniejszy. Dzięki testom możesz upewnić się, że twoje prototypowe rozszerzenia nie powodują żadnych konfliktów z używanymi bibliotekami, ponieważ to Ty decydujesz, jakich bibliotek używasz. To samo nie dotyczy obiektów natywnych, przy założeniu, że kod działa w przeglądarce. Jeśli zdefiniujesz Array.prototype.swizzle(foo, bar)dzisiaj, a jutro Google doda Array.prototype.swizzle(bar, foo)do Chrome, możesz skończyć z niektórymi zdezorientowanymi kolegami, którzy zastanawiają się, dlaczego .swizzlezachowanie wydaje się nie pasować do tego, co udokumentowano w MDN.

(Zobacz także historię o tym, jak majsterkowanie mootools przy prototypach, których nie posiadali, zmusiło do zmiany nazwy metody ES6, aby uniknąć niszczenia sieci ).

Można tego uniknąć, używając przedrostka specyficznego dla aplikacji dla metod dodawanych do obiektów natywnych (np. Definiuj Array.prototype.myappSwizzlezamiast Array.prototype.swizzle), ale to trochę brzydkie; równie dobrze można go rozwiązać za pomocą samodzielnych funkcji narzędziowych zamiast rozszerzania prototypów.

Mark Amery
źródło
2

Perfekcyjny jest również powodem. Czasami może być konieczne zapętlenie kluczy. Można to zrobić na kilka sposobów

for (let key in object) { ... }
for (let key in object) { if (object.hasOwnProperty(key) { ... } }
for (let key of Object.keys(object)) { ... }

Zwykle używam, for of Object.keys()ponieważ robi to dobrze i jest stosunkowo zwięzły, nie ma potrzeby dodawania czeku.

Ale jest znacznie wolniejszy .

wyniki for-of vs for-in perf

Zgadywanie, że powód Object.keysjest powolny, jest oczywiste, Object.keys()trzeba dokonać alokacji. W rzeczywistości AFAIK musi przydzielić kopię wszystkich kluczy od tego czasu.

  const before = Object.keys(object);
  object.newProp = true;
  const after = Object.keys(object);

  before.join('') !== after.join('')

Możliwe, że silnik JS mógłby użyć jakiejś niezmiennej struktury klucza, aby zwrócić Object.keys(object)odniesienie coś, co iteruje po niezmiennych kluczach i object.newProptworzy całkowicie nowy niezmienny obiekt kluczy, ale cokolwiek, jest wyraźnie wolniejsze nawet 15x

Nawet sprawdzanie hasOwnPropertyjest do 2x wolniejsze.

Chodzi o to, że jeśli masz kod wrażliwy na perfekcję i potrzebujesz pętli po klawiszach, chcesz mieć możliwość korzystania z niego for inbez konieczności dzwonienia hasOwnProperty. Możesz to zrobić tylko wtedy, gdy nie dokonałeś modyfikacjiObject.prototype

zwróć uwagę, że jeśli Object.definePropertyzmodyfikujesz prototyp, jeśli dodasz elementy, których nie da się wyliczyć, nie wpłyną one na zachowanie JavaScript w powyższych przypadkach. Niestety, przynajmniej w Chrome 83 wpływają one na wydajność.

wprowadź opis obrazu tutaj

Dodałem 3000 niewliczalnych właściwości, aby wymusić pojawienie się jakichkolwiek problemów z perfekcją. Przy zaledwie 30 właściwościach testy były zbyt bliskie, aby stwierdzić, czy wystąpił jakikolwiek wpływ na wydajność.

https://jsperf.com/does-adding-non-enumerable-properties-affect-perf

Firefox 77 i Safari 13.1 nie wykazały żadnej różnicy w wydajności między klasami rozszerzonymi i nieugmentowanymi, być może wersja 8 zostanie naprawiona w tym obszarze i możesz zignorować problemy z perf.

Ale dodam też, że istnieje historiaArray.prototype.smoosh . Krótka wersja to Mootools, popularna biblioteka, stworzona we własnym zakresie Array.prototype.flatten. Kiedy komitet normalizacyjny próbował dodać rodzimego Array.prototype.flatten, okazało się, że nie jest to możliwe bez niszczenia wielu witryn. Twórcy, którzy dowiedzieli się o przerwie, zasugerowali nazwanie metody es5 smooshżartem, ale ludzie przestraszyli się, nie rozumiejąc, że to żart. flatZamiast tego zdecydowali sięflatten

Morał z tej historii jest taki, że nie powinieneś rozszerzać rodzimych obiektów. Jeśli to zrobisz, możesz napotkać ten sam problem z psuciem się plików i jeśli Twoja konkretna biblioteka nie będzie tak popularna jak MooTools, dostawcy przeglądarek raczej nie obejdą problemu, który spowodowałeś. Jeśli Twoja biblioteka stanie się tak popularna, byłoby to w pewnym sensie wymuszenie na wszystkich innych rozwiązaniu problemu, który spowodowałeś. Dlatego proszę nie rozszerzać natywnych obiektów

gman
źródło
Czekaj, hasOwnPropertyinformuje, czy właściwość jest w obiekcie, czy w prototypie. Ale for inpętla dostarcza tylko wyliczalne właściwości. Właśnie wypróbowałem to: dodaj niewliczalną właściwość do prototypu Array, a nie pojawi się ona w pętli. Nie ma potrzeby, nie zmodyfikować prototyp najprostszej pętli do pracy.
ygoe
Ale to wciąż zła praktyka z innych powodów;)
gman