web-dev-qa-db-fra.com

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

Tous les leaders d'opinion du JS disent que l'extension des objets natifs est une mauvaise pratique. Mais pourquoi? Avons-nous un succès? Craignent-ils que quelqu'un le fasse "de la mauvaise manière" et ajoute des types énumérables à Object, détruisant pratiquement toutes les boucles d'un objet?

Prenez TJ Holowaychuk 's should.js par exemple. Il ajoute un simple getter à Object et tout fonctionne bien ( source ).

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

Cela fait 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?

102
buschtoens

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 acceptable. Mais si 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 object et array 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 est à l’origine de toutes sortes de bugs terribles en javascript.

Si vous avez besoin d'un comportement personnalisé, il est de loin 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 modifier le fonctionnement d’une classe sans la sous-classer est une caractéristique importante de tout bon langage de programmation, mais c’est une option qui doit être utilisée rarement et avec prudence.

96
Abhi Beckert

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

L'argument principal: Il a l'air mieux et est plus intuitif: sucre de syntaxe. C'est une fonction spécifique au type/instance, elle devrait donc être spécifiquement liée à ce type/instance.

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

Les deux ont un point. Lorsque vous vous reposez sur deux bibliothèques qui changent directement vos types, vous obtiendrez probablement un code défectueux, car les fonctionnalités attendues ne sont probablement pas les mêmes. Je suis totalement d'accord sur ça. 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 en utilisez seulement 10% et 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, ainsi elles n'interféreront pas. Si 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 prolongez pas les types natifs. Étendez un type natif uniquement si vous êtes sûr à 100% que l'utilisateur final connaîtra et souhaitera ce comportement. Dans no case, manipulez les fonctions existantes d'un type natif, car cela briserait l'interface existante.

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


A l'origine, j'avais posé cette question parce que je voulais que Errors puisse être envoyé via JSON. Donc, j'avais besoin d'un moyen de les renforcer. error.stringify() était bien meilleur que errorlib.stringify(error); comme le suggère la deuxième construction, j'opère sur errorlib et non sur error lui-même.

27
buschtoens

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

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

Eh bien, comment l'auteur peut-il savoir? Et si mon cadre moqueur fait la même chose? Et si mes promesses lib fait la même chose?

Si vous le faites dans votre propre projet, alors ça va. Mais pour une bibliothèque, alors c'est une mauvaise conception. Underscore.js est un exemple de la chose faite de la bonne façon:

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

Si vous le regardez au cas par cas, certaines implémentations sont peut-être acceptables.

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

Le remplacement des méthodes déjà créées crée plus de problèmes qu'il n'en résout, raison pour laquelle il est couramment dit, dans de nombreux langages de programmation, d'éviter cette pratique. Comment les développeurs doivent-ils savoir 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 ce post a mentionné comment le nouveau dev doit-il savoir si cette méthode fait partie du JS principal ou s’il faut 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 pouvant entrer en conflit avec d’autres bibliothèques, vous utilisiez un modificateur spécifique à la société/à l’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 , ce qui les informerait qu'il s'agissait d'une extension spécifique à une société/application.

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

9
SoEzPz

L'extension de prototypes de composants intégrés est en effet une mauvaise idée. Cependant, ES2015 a introduit une nouvelle technique pouvant être utilisée pour obtenir le comportement souhaité:

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

L'implémentation suivante étend les prototypes Number et Array sans 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 constater, même les prototypes primitifs peuvent être étendus par cette technique. Il utilise une structure de carte et une identité Object pour associer des types à des prototypes intégrés.

Mon exemple active une fonction reduce qui n'attend qu'un seul argument Array, car elle peut extraire les informations sur la création d'un accumulateur vide et la concaténation d'éléments avec cet accumulateur à partir des éléments du tableau lui-même.

Veuillez noter que j'aurais pu utiliser le type Map 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, une WeakMap n'est pas itérable et ne peut être inspectée que si vous avez la bonne clé. C'est une fonctionnalité souhaitée, car je souhaite éviter toute forme de réflexion de type.

6
user6445533

Une raison de plus pour laquelle vous 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 que de gros problèmes commencent.

Nous avons introduit Webcomponents sur l'une de nos pages, et donc webcomponents-lite.js décide de remplacer l'ensemble de l'objet événement (natif) dans IE (pourquoi?) . Ceci casse bien sûr prototype.js ce qui casse Magento . (jusqu'à ce que vous trouviez le problème, vous pouvez passer de nombreuses heures à le localiser)

Si vous aimez les ennuis, continuez!

3
Eugen Wesseloh

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

  1. Si vous le faites mal, vous ajouterez accidentellement une propriété énumérable à tous les objets du type étendu. Facilement contourné en utilisant Object.defineProperty , qui crée par défaut des propriétés non énumérables.
  2. Vous pourriez 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, consultez les notes de publication lors de la mise à niveau et testez votre application.
  3. Vous pourriez provoquer un conflit avec une version future de l'environnement JavaScript natif.

Le point 3 est sans doute le plus important. Grâce aux tests, vous pouvez vous assurer que vos extensions de prototype ne causent 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 que Google ajoute Array.prototype.swizzle(bar, foo) à Chrome, vous risquez de vous retrouver avec des collègues confus qui se demandent pourquoi le comportement de .swizzle ne semble pas correspondre à ce qui est documenté sur MDN.

(Voir aussi le l'histoire de la façon dont les outils de prototypage qu'ils ne possédaient pas chez mootools ont obligé à renommer une méthode ES6 pour éviter de rompre 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 Array.prototype.myappSwizzle au lieu de Array.prototype.swizzle), mais c'est un peu moche; il est tout aussi facile de le résoudre en utilisant des fonctions utilitaires autonomes au lieu d'augmenter les prototypes.

1
Mark Amery