javascript - De ce este o practică rea extinderea obiectelor native?


Fiecare lider de opinie JS spune că extinderea obiectelor native este o practică proastă. Dar de ce? Primim o scădere a performanței? Se tem că cineva o va face „în mod greșit” și va adăuga tipuri enumerabile la Object, practic distrugând toate buclele pe orice obiect?

Luați ca exemplu should.js de TJ Holowaychuk. El adaugă un getter simplu la Object și totul funcționează bine (sursă).

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

Asta are cu adevărat sens. De exemplu, cineva ar putea extinde Array.

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

Există argumente împotriva extinderii tipurilor native?

Răspuns #1

Când extinzi un obiect, îi schimbi comportamentul.

Schimbarea comportamentului unui obiect care va fi folosit doar de codul tău este în regulă. Dar atunci când schimbi comportamentul a ceva care este folosit și de alt cod există riscul să strici acel alt cod.

Când metoda este adăugată claselor de obiecte și array în JavaScript, riscul de a strica ceva este foarte mare - datorită modului în care funcționează JavaScript. Ani lungi de experiență m-au învățat că acest gen de lucruri cauzează toate felurile de erori groaznice în JavaScript.

Dacă ai nevoie de un comportament personalizat, este mult mai bine să definești propria ta clasă (poate o subclasă) în loc să modifici una nativă. În felul acesta nu vei strica nimic deloc.

Abilitatea de a schimba modul în care funcționează o clasă fără a crea subclase este o caracteristică importantă a oricărui limbaj de programare bun, dar este una care trebuie folosită rar și cu prudență.

Răspunsul #2

Nu există un dezavantaj măsurabil, cum ar fi o scădere a performanței. Cel puțin nimeni nu a menționat vreunul. Deci, acesta este o chestiune de preferință personală și experiențe.

Principalul argument pro: Arată mai bine și este mai intuitiv: zahăr sintactic. Este o funcție specifică unui tip/instanță, deci ar trebui să fie legată în mod specific de acel tip/instanță.

Principalul contraargument: Codul poate interfera. Dacă biblioteca A adaugă o funcție, aceasta ar putea suprascrie funcția bibliotecii B. Acest lucru poate strica codul foarte ușor.

Amândoi au un punct. Când te bazezi pe două biblioteci care îți modifică direct tipurile, cel mai probabil vei ajunge cu codul stricat, deoarece funcționalitatea așteptată probabil nu este aceeași. Sunt total de acord cu asta. Macro-bibliotecile nu trebuie să manipuleze tipurile native. Altfel, tu ca dezvoltator nu vei ști niciodată ce se întâmplă cu adevărat în spatele scenei.

Și acesta este motivul pentru care nu-mi plac biblioteci precum jQuery, Underscore etc. Nu mă înțelegeți greșit; sunt absolut bine programate și funcționează de minune, dar sunt mari. Folosești doar 10% din ele și înțelegi cam 1%.

De aceea prefer o abordare atomistică, unde ai nevoie doar de ceea ce ai cu adevărat nevoie. În acest fel, știi întotdeauna ce se întâmplă. Micro-bibliotecile fac doar ceea ce vrei tu să facă, așa că nu vor interfera. În contextul în care utilizatorul final știe ce funcționalități sunt adăugate, extinderea tipurilor native poate fi considerată sigură.

TL;DR Când ai îndoieli, nu extinde tipurile native. Extinde un tip nativ doar dacă ești 100% sigur că utilizatorul final va cunoaște și va dori acel comportament. În niciun caz nu manipula funcțiile existente ale unui tip nativ, deoarece acest lucru ar strica interfața existentă.

Dacă decizi să extinzi tipul, folosește Object.defineProperty(obj, prop, desc); dacă nu poți, folosește prototype al tipului.

Am venit inițial cu această întrebare pentru că voiam ca Error-urile să poată fi trimise prin JSON. Așadar, aveam nevoie de o modalitate de a le transforma în șiruri. error.stringify() părea mult mai potrivit decât errorlib.stringify(error); așa cum sugerează a doua construcție, operez pe errorlib și nu pe error în sine.

Răspunsul #3

În opinia mea, este o practică proastă. Principalul motiv este integrarea. Citând documentația should.js:

O, Doamne, SE EXTINDE OBIECTUL???!?!@ Da, da, se extinde, cu un singur getter ar trebui, și nu, nu îți va strica codul

Ei bine, cum poate ști autorul? Ce se întâmplă dacă cadrul meu de mock-uri face același lucru? Ce se întâmplă dacă biblioteca mea de promisiuni face același lucru?

Dacă faci asta în propriul tău proiect, atunci este în regulă. Dar pentru o bibliotecă, atunci este un design prost. Underscore.js este un exemplu al lucrului făcut corect:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()

Răspunsul #4

Dacă te uiți la asta caz cu caz, poate că unele implementări sunt acceptabile.

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

Suprascrierea metodelor deja create creează mai multe probleme decât rezolvă, motiv pentru care se afirmă frecvent, în multe limbaje de programare, să se evite această practică. Cum ar trebui să știe dezvoltatorii că funcția a fost modificată?

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

În acest caz nu suprascriem nicio metodă JS de bază cunoscută, dar extindem String. Un argument menționat în această postare a fost cum ar putea noul dezvoltator să știe dacă această metodă face parte din JS de bază sau unde poate găsi documentația? Ce s-ar întâmpla dacă obiectul JS de bază String ar primi o metodă numită capitalize?

Ce-ar fi dacă, în loc să adaugi nume care ar putea intra în coliziune cu alte biblioteci, ai folosi un modificator specific companiei/aplicației pe care toți dezvoltatorii l-ar putea înțelege?

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.

Dacă ai continua să extinzi alte obiecte, toți dezvoltatorii le-ar întâlni în natură cu (în acest caz) noi, ceea ce le-ar semnala că era o extensie specifică companiei/aplicației.

Acest lucru nu elimină coliziunile de nume, dar reduce posibilitatea acestora. Dacă decideți că extinderea obiectelor JS de bază este potrivită pentru dumneavoastră și/sau echipa dumneavoastră, poate că aceasta este pentru voi.

Răspunsul #5

Extinderea prototipurilor obiectelor încorporate este într-adevăr o idee proastă. Totuși, ES2015 a introdus o nouă tehnică care poate fi utilizată pentru a obține comportamentul dorit:

Utilizarea WeakMap-urilor pentru a asocia tipuri cu prototipurile încorporate

Implementarea următoare extinde prototipurile Number și Array fără a le modifica deloc:

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

După cum puteți vedea, chiar și prototipurile primitive pot fi extinse prin această tehnică. Aceasta folosește o structură de hartă și identitatea Object pentru a asocia tipurile cu prototipurile încorporate.

Exemplul meu permite o funcție reduce care așteaptă doar un Array ca singur argument, deoarece poate extrage informațiile despre cum să creeze un acumulator gol și cum să concatenze elementele cu acest acumulator din elementele Array-ului însuși.

Vă rugăm să rețineți că aș fi putut folosi tipul normal Map, deoarece referințele slabe nu au sens atunci când ele reprezintă doar prototipuri încorporate, care nu sunt niciodată colectate de gunoi. Totuși, un WeakMap nu este iterabil și nu poate fi inspectat decât dacă aveți cheia potrivită. Aceasta este o caracteristică dorită, deoarece vreau să evit orice formă de reflecție a tipului.