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 Object
i 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?
javascript
prototype
prototypal-inheritance
buschtoens
źródło
źródło
Odpowiedzi:
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.
źródło
.stgringify()
byłoby uważane za bezpieczne?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 definiujeJSON.stringify()
, najlepiej byłoby sprawdzić, czy to istnieje, a jeśli nie zdefiniować tego samodzielnie.someError.stringify()
więcejerrors.stringify(someError)
. Jest prosty i doskonale pasuje do koncepcji js. Robię coś, co jest ściśle powiązane z określonym obiektem ErrorObject.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 typuprototype
.Pierwotnie wymyśliłem to pytanie, ponieważ chciałem,
Error
aby 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,errorlib
a nie naerror
sobie.źródło
MyError extends Error
może miećstringify
metodę 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 przydatneError
niż w przypadku innych.Moim zdaniem to zła praktyka. Głównym powodem jest integracja. Cytując dokumenty powinny.js:
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()
źródło
_(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 jakfoo(native).coolStuff()
przekształcenie jej w jakiś „rozszerzony” obiekt, wygląda świetnie syntaktycznie. Więc dzięki za to!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.
źródło
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
WeakMap
s do kojarzenia typów z wbudowanymi prototypamiPoniższa implementacja rozszerza prototypy
Number
iArray
bez 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
Object
tożsamości do kojarzenia typów z wbudowanymi prototypami.Mój przykład włącza
reduce
funkcję, która oczekuje tylkoArray
jako 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
Map
typu, ponieważ słabe odwołania nie mają sensu, gdy reprezentują tylko wbudowane prototypy, które nigdy nie są usuwane jako śmieci. JednakWeakMap
nie 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
xs[0]
na puste tablice.type(undefined).monoid ...
Object.prototype
w ten sposób nie może być wywołana na anArray
), 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.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!
źródło
Widzę trzy powody, aby tego nie robić ( przynajmniej z poziomu aplikacji ), z których tylko dwa są omówione w istniejących odpowiedziach tutaj:
Object.defineProperty
, które domyślnie tworzy niewliczalne właściwości.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 dodaArray.prototype.swizzle(bar, foo)
do Chrome, możesz skończyć z niektórymi zdezorientowanymi kolegami, którzy zastanawiają się, dlaczego.swizzle
zachowanie 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.myappSwizzle
zamiastArray.prototype.swizzle
), ale to trochę brzydkie; równie dobrze można go rozwiązać za pomocą samodzielnych funkcji narzędziowych zamiast rozszerzania prototypów.źródło
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 .
Zgadywanie, że powód
Object.keys
jest 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 iobject.newProp
tworzy całkowicie nowy niezmienny obiekt kluczy, ale cokolwiek, jest wyraźnie wolniejsze nawet 15xNawet sprawdzanie
hasOwnProperty
jest 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 in
bez konieczności dzwonieniahasOwnProperty
. Możesz to zrobić tylko wtedy, gdy nie dokonałeś modyfikacjiObject.prototype
zwróć uwagę, że jeśli
Object.defineProperty
zmodyfikujesz 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ść.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 historia
Array.prototype.smoosh
. Krótka wersja to Mootools, popularna biblioteka, stworzona we własnym zakresieArray.prototype.flatten
. Kiedy komitet normalizacyjny próbował dodać rodzimegoArray.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 es5smoosh
żartem, ale ludzie przestraszyli się, nie rozumiejąc, że to żart.flat
Zamiast 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
źródło
hasOwnProperty
informuje, czy właściwość jest w obiekcie, czy w prototypie. Alefor in
pę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.