Perché l'estensione degli oggetti nativi è una cattiva pratica?


136

Ogni opinion leader di JS afferma che l'estensione degli oggetti nativi è una cattiva pratica. Ma perché? Abbiamo un successo nelle prestazioni? Temono che qualcuno faccia "nel modo sbagliato", e aggiunge tipi innumerevoli Object, praticamente distruggendo tutti i cicli su qualsiasi oggetto?

Prendere TJ Holowaychuk 's should.js per esempio. Si aggiunge un semplice getter per Objecte tutto funziona bene ( fonte ).

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

Questo ha davvero senso. Ad esempio, si potrebbe estendere Array.

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

Ci sono argomenti contro l'estensione dei tipi nativi?


9
Cosa ti aspetti che accada quando, in seguito, un oggetto nativo viene modificato per includere una funzione "rimuovi" con una semantica diversa dalla tua? Non controlli lo standard.
ta.speot.is il

1
Non è il tuo tipo nativo. È il tipo nativo di tutti.

6
"Temono che qualcuno faccia" nel modo sbagliato ", e aggiunge tipi enumerabili a Object, praticamente distruggendo tutti i loop su qualsiasi oggetto?" : Sì Ai tempi in cui questa opinione era formata era impossibile creare proprietà non enumerabili. Ora le cose potrebbero essere diverse in questo senso, ma immagina che ogni libreria stia semplicemente estendendo gli oggetti nativi come la vogliono. C'è una ragione per cui abbiamo iniziato a usare gli spazi dei nomi.
Felix Kling,

5
Per quello che vale alcuni "opinion leader", ad esempio Brendan Eich pensa che sia perfettamente perfetto estendere i prototipi nativi.
Benjamin Gruenbaum,

2
Foo non dovrebbe essere globale in questi giorni, abbiamo incluso, requestjs, commonjs ecc.
Jamie Pate

Risposte:


127

Quando estendi un oggetto, ne cambi il comportamento.

La modifica del comportamento di un oggetto che verrà utilizzato solo dal proprio codice va bene. Ma quando cambi il comportamento di qualcosa che è anche usato da un altro codice c'è il rischio che lo spezzi.

Quando si tratta di aggiungere metodi all'oggetto e alle classi di array in javascript, il rischio di interrompere qualcosa è molto elevato, a causa del modo in cui funziona javascript. Lunghi anni di esperienza mi hanno insegnato che questo tipo di cose causa tutti i tipi di terribili bug in javascript.

Se hai bisogno di un comportamento personalizzato, è molto meglio definire la tua classe (forse una sottoclasse) invece di cambiarne una nativa. In questo modo non romperai nulla.

La capacità di cambiare il modo in cui una classe funziona senza sottoclassare è una caratteristica importante di qualsiasi buon linguaggio di programmazione, ma deve essere usata raramente e con cautela.


2
Quindi aggiungere qualcosa del genere .stgringify()sarebbe considerato sicuro?
buschtoens il

7
Ci sono molti problemi, ad esempio cosa succede se qualche altro codice tenta anche di aggiungere il proprio stringify()metodo con comportamenti diversi? Non è davvero qualcosa che dovresti fare nella programmazione quotidiana ... non se tutto ciò che vuoi fare è salvare qualche carattere di codice qua e là. Meglio definire la propria classe o funzione che accetta qualsiasi input e lo stringe. La maggior parte dei browser definisce JSON.stringify(), il migliore sarebbe verificare se esiste, e se non definirlo da soli.
Abhi Beckert,

5
Preferisco someError.stringify()finita errors.stringify(someError). È semplice e si adatta perfettamente al concetto di js. Sto facendo qualcosa che è specificamente legato a un certo ErrorObject.
buschtoens il

1
L'unico (ma buono) argomento che rimane è che alcune enormi lib-lib potrebbero iniziare a prendere il sopravvento sui tuoi tipi e interferire tra loro. O c'è qualcos'altro?
buschtoens il

4
Se il problema è che potresti avere una collisione, potresti usare un modello in cui ogni volta che lo hai fatto, confermi sempre che la funzione non è definita prima di definirla? E se metti sempre librerie di terze parti prima del tuo codice, dovresti essere in grado di rilevare sempre tali collisioni.
Dave Cousineau,

32

Non ci sono inconvenienti misurabili, come un successo nelle prestazioni. Almeno nessuno ne ha menzionato nessuno. Quindi questa è una questione di preferenze ed esperienze personali.

Il principale argomento pro: sembra migliore ed è più intuitivo: lo zucchero sintattico. È una funzione specifica di tipo / istanza, quindi dovrebbe essere specificamente associata a quel tipo / istanza.

Il principale argomento contrario: il codice può interferire. Se lib A aggiunge una funzione, potrebbe sovrascrivere la funzione di lib B. Questo può rompere il codice molto facilmente.

Entrambi hanno un punto. Quando fai affidamento su due librerie che cambiano direttamente i tuoi tipi, molto probabilmente finirai con il codice non funzionante poiché la funzionalità prevista non è probabilmente la stessa. Sono totalmente d'accordo su questo. Le macro-librerie non devono manipolare i tipi nativi. Altrimenti tu come sviluppatore non saprai mai cosa sta succedendo dietro le quinte.

E questo è il motivo per cui non mi piacciono le librerie come jQuery, il trattino basso, ecc. Non fraintendetemi; sono assolutamente ben programmati e funzionano come un fascino, ma sono grandi . Ne usi solo il 10% e ne comprendi circa l'1%.

Ecco perché preferisco un approccio atomistico , in cui hai solo bisogno di ciò di cui hai veramente bisogno. In questo modo, sai sempre cosa succede. Le micro-librerie fanno solo ciò che vuoi che facciano, quindi non interferiranno. Nel contesto in cui l'utente finale sa quali funzionalità vengono aggiunte, l'estensione dei tipi nativi può essere considerata sicura.

TL; DR In caso di dubbio, non estendere i tipi nativi. Estendi un tipo nativo solo se sei sicuro al 100%, che l'utente finale conoscerà e vorrà quel comportamento. Non manipolare in nessun caso le funzioni esistenti di un tipo nativo, in quanto ciò spezzerebbe l'interfaccia esistente.

Se si decide di estendere il tipo, utilizzare Object.defineProperty(obj, prop, desc); se non puoi , usa il tipo prototype.


Inizialmente mi è venuta questa domanda perché volevo Errorche fosse mandabile via JSON. Quindi, avevo bisogno di un modo per renderli più severi. error.stringify()sentito molto meglio di errorlib.stringify(error); come suggerisce il secondo costrutto, sto operando su errorlibe non su errorse stesso.


Sono aperto su ulteriori opinioni su questo.
buschtoens il

4
Stai insinuando che jQuery e il carattere di sottolineatura estendono gli oggetti nativi? Non lo fanno. Quindi, se li stai evitando per questo motivo, ti sbagli.
JLRishe

1
Ecco una domanda che ritengo manchi nell'argomento secondo cui l'estensione degli oggetti nativi con lib A potrebbe essere in conflitto con lib B: Perché dovresti avere due librerie di natura così simile o così ampia da rendere possibile questo conflitto? Vale a dire scegliere o lodash o sottolineare non entrambi. Il nostro attuale panorama libraray in javascript è così eccessivamente saturo e gli sviluppatori (in generale) diventano così negligenti aggiungendoli che finiamo per evitare le migliori pratiche per placare i nostri Lib Lords
micahblu,

2
@micahblu - una libreria potrebbe decidere di modificare un oggetto standard solo per la comodità della propria programmazione interna (questo è spesso il motivo per cui le persone vogliono farlo). Quindi, questo conflitto non è evitato solo perché non useresti due librerie che hanno la stessa funzione.
jfriend00

1
Puoi anche (a partire da es2015) creare una nuova classe che estende una classe esistente e usarla nel tuo codice. quindi se MyError extends Errorpuò avere un stringifymetodo senza scontrarsi con altre sottoclassi. Devi ancora gestire errori non generati dal tuo codice, quindi potrebbe essere meno utile Errorrispetto ad altri.
ShadSterling

16

Secondo me, è una cattiva pratica. Il motivo principale è l'integrazione. Citando i documenti should.js:

OMG ESPENDE L'OGGETTO ???!?! @ Sì, sì, con un solo getter dovrebbe, e no non infrange il tuo codice

Bene, come può sapere l'autore? E se il mio framework beffardo fa lo stesso? E se le mie promesse lib facessero lo stesso?

Se lo stai facendo nel tuo progetto, allora va bene. Ma per una biblioteca, allora è un cattivo design. Underscore.js è un esempio della cosa fatta nel modo giusto:

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

2
Sono sicuro che la risposta di TJ sarebbe di non usare quelle promesse o schemi beffardi: x
Jim Schubert

L' _(arr).flatten()esempio in realtà mi ha convinto a non estendere gli oggetti nativi. La mia ragione personale per farlo era puramente sintattica. Ma questo soddisfa il mio sentimento estetico :) Anche usare un nome di funzione più regolare come foo(native).coolStuff()convertirlo in un oggetto "esteso" sembra sintatticamente fantastico. Quindi grazie per quello!
es.

16

Se lo guardi caso per caso, forse alcune implementazioni sono accettabili.

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

Sovrascrivere i metodi già creati crea più problemi di quanti ne risolva, motivo per cui è comunemente affermato, in molti linguaggi di programmazione, evitare questa pratica. In che modo gli sviluppatori sanno che la funzione è stata modificata?

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

In questo caso non stiamo sovrascrivendo alcun metodo JS core noto, ma stiamo estendendo String. Un argomento in questo post ha menzionato in che modo il nuovo sviluppatore può sapere se questo metodo fa parte del core JS o dove trovare i documenti? Cosa accadrebbe se l'oggetto core JS String dovesse ottenere un metodo chiamato capitalize ?

Che cosa succede se invece di aggiungere nomi che potrebbero scontrarsi con altre librerie, hai usato un modificatore specifico dell'azienda / app che tutti gli sviluppatori potevano capire?

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.

Se continuassi ad estendere altri oggetti, tutti gli sviluppatori li incontrerebbero in natura con (in questo caso) noi , che li avviserebbe che si trattava di un'estensione specifica dell'azienda / app.

Ciò non elimina le collisioni di nomi, ma riduce la possibilità. Se determini che l'estensione degli oggetti JS core è per te e / o il tuo team, forse è per te.


7

L'estensione dei prototipi di built-in è davvero una cattiva idea. Tuttavia, ES2015 ha introdotto una nuova tecnica che può essere utilizzata per ottenere il comportamento desiderato:

Utilizzo di WeakMaps per associare tipi a prototipi integrati

La seguente implementazione estende i prototipi Numbere Arraysenza toccarli affatto:

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

Come puoi vedere anche i prototipi primitivi possono essere estesi con questa tecnica. Utilizza una struttura e Objectun'identità della mappa per associare i tipi ai prototipi incorporati.

Il mio esempio abilita una reducefunzione che si aspetta solo Arraycome unico argomento, poiché può estrarre le informazioni su come creare un accumulatore vuoto e su come concatenare elementi con questo accumulatore dagli elementi dell'array stesso.

Si noti che avrei potuto usare il Maptipo normale , poiché i riferimenti deboli non hanno senso quando si limitano a rappresentare prototipi incorporati, che non vengono mai raccolti. Tuttavia, a WeakMapnon è iterabile e non può essere ispezionato a meno che tu non abbia la chiave giusta. Questa è una caratteristica desiderata, dal momento che voglio evitare qualsiasi forma di riflessione del tipo.


Questo è un ottimo uso di WeakMap. Fai attenzione a ciò xs[0]su array vuoti. type(undefined).monoid ...
Grazie

@naomik Lo so - vedi la mia ultima domanda il 2 ° frammento di codice.

Quanto è standard il supporto per questo @ftor?
onassar,

5
-1; qual è il punto di tutto ciò? Stai solo creando un dizionario globale separato che associa i tipi ai metodi e quindi cerca esplicitamente i metodi in quel dizionario per tipo. Di conseguenza, si perde il supporto per l'ereditarietà (un metodo "aggiunto" Object.prototypein questo modo non può essere richiamato su un Array) e la sintassi è significativamente più lunga / più brutta che se si estendesse realmente un prototipo. Sarebbe quasi sempre più semplice creare solo classi di utilità con metodi statici; l'unico vantaggio di questo approccio è un supporto limitato al polimorfismo.
Mark Amery,

4
@MarkAmery Ehi amico, non capisci. Non perdo l'ereditarietà, ma mi libero. L'eredità è così anni '80, dovresti dimenticartene. Il punto centrale di questo hack è imitare le classi di tipi, che, in poche parole, sono funzioni sovraccariche. Esistono tuttavia legittime critiche: è utile imitare le classi di tipi in Javascript? No, non lo è. Piuttosto usa un linguaggio tipizzato che li supporti in modo nativo. Sono abbastanza sicuro che questo è ciò che intendevi.

7

Un motivo in più per cui si dovrebbe non estendersi oggetti nativi:

Usiamo Magento che utilizza prototype.js e estende molte cose su oggetti nativi. Funziona bene fino a quando non decidi di ottenere nuove funzionalità ed è qui che iniziano i grandi problemi.

Abbiamo introdotto Webcomponents su una delle nostre pagine, quindi webcomponents-lite.js decide di sostituire l'intero oggetto evento (nativo) in IE (perché?). Questo ovviamente rompe prototype.js che a sua volta rompe Magento. (fino a quando non trovi il problema, puoi investire molte ore a rintracciarlo)

Se ti piacciono i problemi, continua a farlo!


4

Vedo tre motivi per non farlo (all'interno di un'applicazione , almeno), solo due dei quali sono affrontati nelle risposte esistenti qui:

  1. Se lo fai in modo sbagliato, aggiungerai accidentalmente una proprietà enumerabile a tutti gli oggetti del tipo esteso. Facilmente aggirabile usando Object.defineProperty, che crea proprietà non enumerabili per impostazione predefinita.
  2. Potresti causare un conflitto con una libreria che stai utilizzando. Può essere evitato con diligenza; controlla quali metodi definiscono le librerie che usi prima di aggiungere qualcosa a un prototipo, controlla le note di rilascio durante l'aggiornamento e testa la tua applicazione.
  3. Potresti causare un conflitto con una versione futura dell'ambiente JavaScript nativo.

Il punto 3 è probabilmente il più importante. Si può fare in modo, attraverso test, che le estensioni prototipo non causano conflitti con le librerie che si usa, perché si decide quali librerie si utilizza. Lo stesso non vale per gli oggetti nativi, supponendo che il codice venga eseguito in un browser. Se lo definisci Array.prototype.swizzle(foo, bar)oggi e domani Google aggiunge Array.prototype.swizzle(bar, foo)a Chrome, potresti finire con alcuni colleghi confusi che si chiedono perché .swizzleil comportamento non sembra corrispondere a quanto documentato su MDN.

(Vedi anche la storia di come i mootools giocherellavano con i prototipi che non possedevano costringevano un metodo ES6 a essere rinominato per evitare di rompere il web .)

Questo è evitabile usando un prefisso specifico dell'applicazione per i metodi aggiunti agli oggetti nativi (ad esempio, definire Array.prototype.myappSwizzleinvece di Array.prototype.swizzle), ma è un po 'brutto; è altrettanto risolvibile utilizzando le funzioni di utilità standalone invece di aumentare i prototipi.


2

Perf è anche un motivo. A volte potresti dover ricorrere ai tasti. Esistono diversi modi per farlo

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

Di solito uso for of Object.keys()come fa la cosa giusta ed è relativamente conciso, non è necessario aggiungere il controllo.

Ma è molto più lento .

risultati perf for-of vs for-in

Indovinare il motivo Object.keysè lento è ovvio, Object.keys()deve fare un'allocazione. Infatti, AFAIK deve allocare una copia di tutte le chiavi da allora.

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

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

È possibile che il motore JS possa utilizzare una sorta di struttura di chiavi immutabile in modo che Object.keys(object)restituisca un riferimento qualcosa che scorre su chiavi immutabili e che object.newPropcrea un oggetto chiavi immutabile completamente nuovo ma, qualunque cosa, è chiaramente più lento fino a 15 volte

Anche il controllo hasOwnPropertyè fino a 2 volte più lento.

Il punto di tutto ciò è che se si dispone di un codice sensibile al perf e è necessario eseguire il loop over dei tasti, si desidera essere in grado di utilizzare for insenza dover chiamare hasOwnProperty. Puoi farlo solo se non hai modificatoObject.prototype

nota che se usi Object.definePropertyper modificare il prototipo se le cose che aggiungi non sono enumerabili, non influenzeranno il comportamento di JavaScript nei casi precedenti. Sfortunatamente, almeno in Chrome 83, influiscono sulle prestazioni.

inserisci qui la descrizione dell'immagine

Ho aggiunto 3000 proprietà non enumerabili solo per provare a forzare la comparsa di eventuali problemi perf. Con solo 30 proprietà i test erano troppo vicini per dire se ci fosse un impatto perfetto.

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

Firefox 77 e Safari 13.1 non hanno mostrato alcuna differenza in perf tra le classi Aumentata e Non aumentata, forse v8 verrà risolto in quest'area e puoi ignorare i problemi di perf.

Ma, lasciatemi aggiungere anche che c'è la storia diArray.prototype.smoosh . La versione breve è Mootools, una biblioteca popolare, fatta propria Array.prototype.flatten. Quando il comitato per gli standard ha tentato di aggiungere un nativo Array.prototype.flatten, ha scoperto che non poteva senza rompere molti siti. Gli sviluppatori che hanno scoperto la pausa hanno suggerito di nominare il metodo es5 smooshcome uno scherzo, ma la gente ha dato di matto non capendo che era uno scherzo. Si stabilirono flatinvece diflatten

La morale della storia è che non dovresti estendere gli oggetti nativi. Se lo fai, potresti imbatterti nello stesso problema della tua interruzione e, a meno che la tua particolare libreria non sia così popolare come MooTools, è improbabile che i fornitori di browser risolvano il problema che hai causato. Se la tua libreria diventa così popolare, sarebbe un po 'meschino costringere tutti gli altri a risolvere il problema che hai causato. Quindi, per favore non estendere gli oggetti nativi


Aspetta, hasOwnPropertyti dice se la proprietà è nell'oggetto o nel prototipo. Ma il for inciclo ti dà solo proprietà enumerabili. Ho appena provato questo: aggiungi una proprietà non enumerabile al prototipo dell'array e non verrà visualizzato nel ciclo. Non c'è bisogno , non di modificare il prototipo per il ciclo più semplice per il lavoro.
giovedì

Ma è ancora una cattiva pratica per altri motivi;)
Gman
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.