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 , à Object
tout 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?
javascript
prototype
prototypal-inheritance
buschtoens
la source
la source
Réponses:
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.
la source
.stgringify()
serait considéré comme sûr?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éfinissentJSON.stringify()
, le mieux serait de vérifier si cela existe, et sinon de le définir vous-même.someError.stringify()
pluserrors.stringify(someError)
. C'est simple et convient parfaitement au concept de js. Je fais quelque chose qui est spécifiquement lié à un certain ErrorObject.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 typeprototype
.J'ai initialement posé cette question parce que je voulais que les
Error
s puissent être envoyés via JSON. Donc, j'avais besoin d'un moyen de les stringifier.error.stringify()
senti bien mieux queerrorlib.stringify(error)
; comme le suggère la deuxième construction, j'opèreerrorlib
surerror
elle-même et non sur elle-même.la source
MyError extends Error
peut avoir unestringify
mé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 avecError
qu'avec d'autres.À mon avis, c'est une mauvaise pratique. La raison principale est l'intégration. Citant les documents should.js:
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:
la source
_(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, commefoo(native).coolStuff()
pour le convertir en un objet "étendu", la syntaxe est excellente. Alors merci pour ça!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?
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?
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.
la source
É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
WeakMap
s pour associer des types à des prototypes intégrésLa mise en œuvre suivante étend les prototypes
Number
etArray
sans les toucher du tout:Comme vous pouvez le voir, même les prototypes primitifs peuvent être étendus par cette technique. Il utilise une structure et une
Object
identité de carte pour associer des types à des prototypes intégrés.Mon exemple active une
reduce
fonction qui n'attend qu'unArray
comme 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
Map
type 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, aWeakMap
n'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
xs[0]
sur les tableaux vides tho.type(undefined).monoid ...
Object.prototype
de cette manière ne peut pas être appelée sur unArray
), 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.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!
la source
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:
Object.defineProperty
, qui crée des propriétés non énumérables par défaut.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 ajouteArray.prototype.swizzle(bar, foo)
à Chrome, vous risquez de vous retrouver avec des collègues confus qui se demandent pourquoi.swizzle
le 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.myappSwizzle
place deArray.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.la source
Perf est aussi une raison. Parfois, vous devrez peut-être boucler sur les clés. Il y a plusieurs moyens de le faire
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 .
Deviner simplement que la raison
Object.keys
est lente est évidente,Object.keys()
il faut faire une allocation. En fait AFAIK il doit allouer une copie de toutes les clés depuis.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 quiobject.newProp
crée un tout nouvel objet de clés immuables, mais quoi qu'il en soit, il est clairement plus lent jusqu'à 15xMême la vérification
hasOwnProperty
est 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 in
sans avoir à appelerhasOwnProperty
. Vous ne pouvez le faire que si vous n'avez pas modifiéObject.prototype
notez que si vous utilisez
Object.defineProperty
pour 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.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 de
Array.prototype.smoosh
. La version courte est Mootools, une bibliothèque populaire, faite leur propreArray.prototype.flatten
. Lorsque le comité des normes a essayé d'ajouter un natif,Array.prototype.flatten
il 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 es5smoosh
comme une blague, mais les gens ont paniqué sans comprendre que c'était une blague. Ils se sont installés auflat
lieu 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
la source
hasOwnProperty
vous indique si la propriété est dans l'objet ou dans le prototype. Mais lafor in
boucle 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.