Pourquoi l'extension des objets natifs est-elle une mauvaise pratique?

136

Chaque leader d'opinion JS dit que l'extension des objets natifs est une mauvaise pratique. Mais pourquoi? Avons-nous un hit de performance? Craignent-ils que quelqu'un le fasse "dans le mauvais sens", et ajoute des types énumérables Object, détruisant pratiquement toutes les boucles sur n'importe quel objet?

Prenez TJ Holowaychuk de » de should.js par exemple. Il ajoute un getter simple , à Objecttout et fonctionne très bien ( la source ).

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

Cela a vraiment du sens. Par exemple, on pourrait étendre Array.

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

Existe-t-il des arguments contre l'extension des types natifs?

buschtoens
la source
9
Qu'espérez-vous qu'il se passe quand, plus tard, un objet natif sera modifié pour inclure une fonction «supprimer» avec une sémantique différente de la vôtre? Vous ne contrôlez pas la norme.
ta.speot.is
1
Ce n'est pas votre type natif. C'est le type natif de tout le monde.
6
"Ont-ils peur que quelqu'un le fasse" dans le mauvais sens ", et ajoute des types énumérables à Object, détruisant pratiquement toutes les boucles sur n'importe quel objet?" : Ouais. À l'époque où cette opinion se formait, il était impossible de créer des propriétés non énumérables. Maintenant, les choses peuvent être différentes à cet égard, mais imaginez que chaque bibliothèque ne fait qu'étendre les objets natifs comme elle le souhaite. Il y a une raison pour laquelle nous avons commencé à utiliser des espaces de noms.
Felix Kling
5
Pour ce que ça vaut, certains "leaders d'opinion" par exemple Brendan Eich pensent que c'est parfaitement bien d'étendre les prototypes natifs.
Benjamin Gruenbaum le
2
Foo ne devrait pas être global de nos jours, nous avons include, requirejs, commonjs etc.
Jamie Pate

Réponses:

127

Lorsque vous étendez un objet, vous modifiez son comportement.

Changer le comportement d'un objet qui ne sera utilisé que par votre propre code est très bien. Mais lorsque vous modifiez le comportement de quelque chose qui est également utilisé par un autre code, vous risquez de casser cet autre code.

Quand il s'agit d'ajouter des méthodes aux classes d'objets et de tableaux en javascript, le risque de casser quelque chose est très élevé, en raison du fonctionnement de javascript. De longues années d'expérience m'ont appris que ce genre de choses provoque toutes sortes de bugs terribles en javascript.

Si vous avez besoin d'un comportement personnalisé, il est préférable de définir votre propre classe (peut-être une sous-classe) au lieu de changer une classe native. De cette façon, vous ne casserez rien du tout.

La possibilité de changer le fonctionnement d'une classe sans la sous-classer est une caractéristique importante de tout bon langage de programmation, mais c'est une caractéristique qui doit être utilisée rarement et avec précaution.

Abhi Beckert
la source
2
Donc, ajouter quelque chose comme .stgringify()serait considéré comme sûr?
buschtoens
7
Il y a beaucoup de problèmes, par exemple que se passe-t-il si un autre code essaie également d'ajouter sa propre stringify()méthode avec un comportement différent? Ce n'est vraiment pas quelque chose que vous devriez faire dans la programmation quotidienne ... pas si tout ce que vous voulez faire est de sauvegarder quelques caractères de code ici et là. Mieux vaut définir votre propre classe ou fonction qui accepte n'importe quelle entrée et la stringifie. La plupart des navigateurs définissent JSON.stringify(), le mieux serait de vérifier si cela existe, et sinon de le définir vous-même.
Abhi Beckert
5
Je préfère someError.stringify()plus errors.stringify(someError). C'est simple et convient parfaitement au concept de js. Je fais quelque chose qui est spécifiquement lié à un certain ErrorObject.
buschtoens
1
Le seul (mais bon) argument qui reste est que certaines énormes macro-bibliothèques pourraient commencer à prendre le dessus sur vos types et peuvent interférer les unes avec les autres. Ou y a-t-il autre chose?
buschtoens
4
Si le problème est que vous pourriez avoir une collision, pourriez-vous utiliser un modèle où à chaque fois que vous faites cela, vous confirmez toujours que la fonction n'est pas définie avant de la définir? Et si vous placez toujours les bibliothèques tierces avant votre propre code, vous devriez toujours pouvoir détecter de telles collisions.
Dave Cousineau du
32

Il n'y a pas d'inconvénient mesurable, comme un coup de performance. Au moins personne n'en a mentionné. C'est donc une question de préférence personnelle et d'expériences.

Le principal argument des pros: il est plus beau et plus intuitif: le sucre de syntaxe. Il s'agit d'une fonction spécifique à un type / instance, elle doit donc être spécifiquement liée à ce type / instance.

Le principal argument contraire: le code peut interférer. Si la lib A ajoute une fonction, elle pourrait écraser la fonction de la lib B. Cela peut casser le code très facilement.

Les deux ont un point. Lorsque vous comptez sur deux bibliothèques qui changent directement vos types, vous vous retrouverez probablement avec du code défectueux car la fonctionnalité attendue n'est probablement pas la même. Je suis tout à fait d'accord là-dessus. Les macro-bibliothèques ne doivent pas manipuler les types natifs. Sinon, en tant que développeur, vous ne saurez jamais ce qui se passe réellement dans les coulisses.

Et c'est la raison pour laquelle je n'aime pas les bibliothèques comme jQuery, underscore, etc. Ne vous méprenez pas; ils sont absolument bien programmés et fonctionnent à merveille, mais ils sont gros . Vous n'en utilisez que 10% et vous en comprenez environ 1%.

C'est pourquoi je préfère une approche atomistique , où vous n'avez besoin que de ce dont vous avez vraiment besoin. De cette façon, vous savez toujours ce qui se passe. Les micro-bibliothèques ne font que ce que vous voulez qu'elles fassent, elles n'interféreront donc pas. Dans le contexte où l'utilisateur final sait quelles fonctionnalités sont ajoutées, l'extension des types natifs peut être considérée comme sûre.

TL; DR En cas de doute, ne pas étendre les types natifs. N'étendez un type natif que si vous êtes sûr à 100% que l'utilisateur final connaîtra et voudra ce comportement. En aucun cas, ne manipulez les fonctions existantes d'un type natif, car cela casserait l'interface existante.

Si vous décidez d'étendre le type, utilisez Object.defineProperty(obj, prop, desc); si vous ne pouvez pas , utilisez le type prototype.


J'ai initialement posé cette question parce que je voulais que les Errors puissent être envoyés via JSON. Donc, j'avais besoin d'un moyen de les stringifier. error.stringify()senti bien mieux que errorlib.stringify(error); comme le suggère la deuxième construction, j'opère errorlibsur errorelle-même et non sur elle-même.

buschtoens
la source
Je suis ouvert à d'autres opinions à ce sujet.
buschtoens
4
Voulez-vous dire que jQuery et le soulignement étendent les objets natifs? Ils ne le font pas. Donc, si vous les évitez pour cette raison, vous vous trompez.
JLRishe
1
Voici une question qui, je crois, est manquée dans l'argument selon lequel l'extension des objets natifs avec la lib A peut entrer en conflit avec la lib B: Pourquoi auriez-vous deux bibliothèques qui sont soit de nature si similaire, soit de nature si large que ce conflit est possible? C'est à dire que je choisis soit lodash, soit je ne souligne pas les deux. Notre paysage actuel de libraray en javascript est tellement saturé et les développeurs (en général) deviennent si négligents en les ajoutant que nous
finissons
2
@micahblu - une bibliothèque pourrait décider de modifier un objet standard juste pour la commodité de sa propre programmation interne (c'est souvent pourquoi les gens veulent faire cela). Donc, ce conflit n'est pas évité simplement parce que vous n'utiliseriez pas deux bibliothèques qui ont la même fonction.
jfriend00
1
Vous pouvez également (à partir de es2015) créer une nouvelle classe qui étend une classe existante et l'utiliser dans votre propre code. donc s'il MyError extends Errorpeut avoir une stringifyméthode sans entrer en collision avec d'autres sous-classes. Vous devez toujours gérer les erreurs non générées par votre propre code, donc cela peut être moins utile avec Errorqu'avec d'autres.
ShadSterling
16

À mon avis, c'est une mauvaise pratique. La raison principale est l'intégration. Citant les documents should.js:

OMG IT EXTENDS OBJECT ???!?! @ Oui, oui, avec un seul getter devrait, et non, cela ne cassera pas votre code

Eh bien, comment l'auteur peut-il savoir? Et si mon framework moqueur fait de même? Et si mes promesses lib faisaient de même?

Si vous le faites dans votre propre projet, tout va bien. Mais pour une bibliothèque, c'est une mauvaise conception. Underscore.js est un exemple de ce qui est fait de la bonne façon:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Stefan
la source
2
Je suis sûr que la réponse de TJ serait de ne pas utiliser ces promesses ou ces cadres moqueurs: x
Jim Schubert
L' _(arr).flatten()exemple m'a en fait convaincu de ne pas étendre les objets natifs. Ma raison personnelle de le faire était purement syntaxique. Mais cela satisfait mon sentiment esthétique :) Même en utilisant un nom de fonction plus régulier, comme foo(native).coolStuff()pour le convertir en un objet "étendu", la syntaxe est excellente. Alors merci pour ça!
egst
16

Si vous l'examinez au cas par cas, certaines implémentations sont peut-être acceptables.

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

L'écrasement des méthodes déjà créées crée plus de problèmes qu'il n'en résout, c'est pourquoi il est couramment indiqué, dans de nombreux langages de programmation, d'éviter cette pratique. Comment les développeurs savent-ils que la fonction a été modifiée?

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

Dans ce cas, nous n'écrasons aucune méthode JS de base connue, mais nous étendons String. Un argument dans cet article a mentionné comment le nouveau développeur peut-il savoir si cette méthode fait partie du noyau JS, ou où trouver la documentation? Que se passerait-il si l'objet JS String principal obtenait une méthode nommée capitalize ?

Et si, au lieu d'ajouter des noms susceptibles d'entrer en conflit avec d'autres bibliothèques, vous utilisiez un modificateur spécifique à l'entreprise / application que tous les développeurs pourraient comprendre?

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.

Si vous continuiez à étendre d'autres objets, tous les développeurs les rencontreraient dans la nature avec (dans ce cas) nous , qui les avertirait qu'il s'agissait d'une extension spécifique à une entreprise / application.

Cela n'élimine pas les collisions de noms, mais réduit la possibilité. Si vous déterminez que l'extension des objets JS de base est pour vous et / ou votre équipe, c'est peut-être pour vous.

SoEzPz
la source
7

Étendre des prototypes de modules intégrés est en effet une mauvaise idée. Cependant, ES2015 a introduit une nouvelle technique qui peut être utilisée pour obtenir le comportement souhaité:

Utilisation de WeakMaps pour associer des types à des prototypes intégrés

La mise en œuvre suivante étend les prototypes Numberet Arraysans les toucher du tout:

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

Comme vous pouvez le voir, même les prototypes primitifs peuvent être étendus par cette technique. Il utilise une structure et une Objectidentité de carte pour associer des types à des prototypes intégrés.

Mon exemple active une reducefonction qui n'attend qu'un Arraycomme argument unique, car elle peut extraire les informations sur la manière de créer un accumulateur vide et de concaténer des éléments avec cet accumulateur à partir des éléments du tableau lui-même.

Veuillez noter que j'aurais pu utiliser le Maptype normal , car les références faibles n'ont pas de sens lorsqu'elles représentent simplement des prototypes intégrés, qui ne sont jamais récupérés. Cependant, a WeakMapn'est pas itérable et ne peut être inspecté que si vous avez la bonne clé. C'est une fonctionnalité souhaitée, car je souhaite éviter toute forme de réflexion de type.


la source
Ceci est une bonne utilisation de WeakMap. Soyez prudent avec cela xs[0]sur les tableaux vides tho. type(undefined).monoid ...
Merci
@naomik je sais - voir ma dernière question le deuxième extrait de code.
Quelle est la norme de support pour ce @ftor?
onassar
5
-1; quel est le point de tout cela? Vous créez simplement un dictionnaire global distinct qui mappe les types aux méthodes, puis vous recherchez explicitement les méthodes de ce dictionnaire par type. En conséquence, vous perdez le support de l'héritage (une méthode "ajoutée" à Object.prototypede cette manière ne peut pas être appelée sur un Array), et la syntaxe est beaucoup plus longue / moche que si vous étendiez réellement un prototype. Il serait presque toujours plus simple de créer simplement des classes utilitaires avec des méthodes statiques; le seul avantage de cette approche est une prise en charge limitée du polymorphisme.
Mark Amery
4
@MarkAmery Salut mon pote, tu ne comprends pas. Je ne perds pas l'héritage mais m'en débarrasse. L'héritage est tellement 80's, vous devriez l'oublier. Le but de ce hack est d'imiter les classes de types, qui, en termes simples, sont des fonctions surchargées. Il y a cependant une critique légitime: est-il utile d'imiter les classes de types en Javascript? Non, ça ne l'est pas. Utilisez plutôt un langage typé qui les prend en charge de manière native. Je suis presque sûr que c'est ce que vous vouliez dire.
7

Une autre raison pour laquelle vous ne devriez pas étendre les objets natifs:

Nous utilisons Magento qui utilise prototype.js et étend beaucoup de choses sur les objets natifs. Cela fonctionne bien jusqu'à ce que vous décidiez d'intégrer de nouvelles fonctionnalités et c'est là que les gros problèmes commencent.

Nous avons introduit Webcomponents sur l'une de nos pages, donc le webcomponents-lite.js décide de remplacer tout l'objet événement (natif) dans IE (pourquoi?). Cela casse bien sûr prototype.js qui, à son tour, brise Magento. (jusqu'à ce que vous trouviez le problème, vous pouvez consacrer beaucoup d'heures à le retrouver)

Si vous aimez les ennuis, continuez à le faire!

Eugen Wesseloh
la source
4

Je peux voir trois raisons de ne pas le faire (à partir d'une application , au moins), dont seulement deux sont abordées dans les réponses existantes ici:

  1. Si vous ne le faites pas correctement, vous ajouterez accidentellement une propriété énumérable à tous les objets du type étendu. Facile à contourner en utilisant Object.defineProperty, qui crée des propriétés non énumérables par défaut.
  2. Vous pouvez provoquer un conflit avec une bibliothèque que vous utilisez. Peut être évité avec diligence; vérifiez simplement quelles méthodes les bibliothèques que vous utilisez définissent avant d'ajouter quelque chose à un prototype, vérifiez les notes de publication lors de la mise à niveau et testez votre application.
  3. Vous pourriez provoquer un conflit avec une future version de l'environnement JavaScript natif.

Le point 3 est sans doute le plus important. Vous pouvez vous assurer, grâce à des tests, que vos extensions prototypes ne provoquent aucun conflit avec les bibliothèques que vous utilisez, car vous décidez quelles bibliothèques vous utilisez. Il n'en va pas de même pour les objets natifs, en supposant que votre code s'exécute dans un navigateur. Si vous définissez Array.prototype.swizzle(foo, bar)aujourd'hui, et demain Google ajoute Array.prototype.swizzle(bar, foo)à Chrome, vous risquez de vous retrouver avec des collègues confus qui se demandent pourquoi .swizzlele comportement de ce dernier ne semble pas correspondre à ce qui est documenté sur MDN.

(Voir aussi l' histoire de la façon dont le tripotage de mootools avec des prototypes qu'ils ne possédaient pas a forcé une méthode ES6 à être renommée pour éviter de briser le Web .)

Ceci est évitable en utilisant un préfixe spécifique à l'application pour les méthodes ajoutées aux objets natifs (par exemple, définir à la Array.prototype.myappSwizzleplace de Array.prototype.swizzle), mais c'est plutôt moche; il est tout aussi bien résolu en utilisant des fonctions utilitaires autonomes au lieu d'augmenter les prototypes.

Mark Amery
la source
2

Perf est aussi une raison. Parfois, vous devrez peut-être boucler sur les clés. Il y a plusieurs moyens de le faire

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

J'utilise généralement for of Object.keys()comme il fait la bonne chose et est relativement concis, pas besoin d'ajouter le chèque.

Mais, c'est beaucoup plus lent .

résultats de performance for-of vs for-in

Deviner simplement que la raison Object.keysest lente est évidente, Object.keys()il faut faire une allocation. En fait AFAIK il doit allouer une copie de toutes les clés depuis.

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

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

Il est possible que le moteur JS utilise une sorte de structure de clé immuable afin de Object.keys(object)renvoyer une référence quelque chose qui itère sur des clés immuables et qui object.newPropcrée un tout nouvel objet de clés immuables, mais quoi qu'il en soit, il est clairement plus lent jusqu'à 15x

Même la vérification hasOwnPropertyest jusqu'à 2 fois plus lente.

Le point de tout cela est que si vous avez du code sensible aux performances et que vous avez besoin de boucler sur des clés, vous voulez pouvoir l'utiliser for insans avoir à appeler hasOwnProperty. Vous ne pouvez le faire que si vous n'avez pas modifiéObject.prototype

notez que si vous utilisez Object.definePropertypour modifier le prototype si les éléments que vous ajoutez ne sont pas énumérables, ils n'affecteront pas le comportement de JavaScript dans les cas ci-dessus. Malheureusement, au moins dans Chrome 83, ils affectent les performances.

entrez la description de l'image ici

J'ai ajouté 3000 propriétés non énumérables juste pour essayer de forcer l'apparition des problèmes de perf. Avec seulement 30 propriétés, les tests étaient trop proches pour dire s'il y avait un impact sur les performances.

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

Firefox 77 et Safari 13.1 n'ont montré aucune différence de performance entre les classes augmentée et non augmentée. Peut-être que la v8 sera corrigée dans ce domaine et vous pouvez ignorer les problèmes de performance.

Mais permettez-moi également d'ajouter qu'il y a l'histoire deArray.prototype.smoosh . La version courte est Mootools, une bibliothèque populaire, faite leur propre Array.prototype.flatten. Lorsque le comité des normes a essayé d'ajouter un natif, Array.prototype.flattenil a trouvé que le ne pouvait pas sans casser de nombreux sites. Les développeurs qui ont découvert la pause ont suggéré de nommer la méthode es5 smooshcomme une blague, mais les gens ont paniqué sans comprendre que c'était une blague. Ils se sont installés au flatlieu deflatten

La morale de l'histoire est que vous ne devez pas étendre les objets natifs. Si vous le faites, vous pourriez rencontrer le même problème de rupture de vos trucs et à moins que votre bibliothèque particulière ne soit aussi populaire que MooTools, il est peu probable que les fournisseurs de navigateurs contournent le problème que vous avez causé. Si votre bibliothèque devient aussi populaire, ce serait un peu moyen de forcer tout le monde à contourner le problème que vous avez causé. Alors, veuillez ne pas étendre les objets natifs

gman
la source
Attendez, hasOwnPropertyvous indique si la propriété est dans l'objet ou dans le prototype. Mais la for inboucle ne vous donne que des propriétés énumérables. Je viens d'essayer ceci: ajoutez une propriété non énumérable au prototype Array et elle n'apparaîtra pas dans la boucle. Pas besoin non de modifier le prototype de la boucle la plus simple au travail.
ygoe
Mais c'est toujours une mauvaise pratique pour d'autres raisons;)
gman