174 votes

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

Tous les leaders d'opinion de JS disent que l'extension des objets natifs est une mauvaise pratique. Mais pourquoi ? Y a-t-il un impact sur les performances ? Ont-ils peur que quelqu'un fasse les choses "à l'envers", et ajoute des types énumérables aux objets natifs ? Object en détruisant pratiquement toutes les boucles sur n'importe quel objet ?

Prenez TJ Holowaychuk 's devrait.js par exemple. Il ajoute un simple getter a Object et tout fonctionne bien ( fuente ).

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 ?

159voto

Abhi Beckert Points 10768

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

Modifier le comportement d'un objet qui ne sera utilisé que par votre propre code, c'est bien. Mais lorsque vous modifiez le comportement d'un objet qui est également utilisé par un autre code, vous risquez de casser cet autre code.

Lorsqu'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 de loin préférable de définir votre propre classe (peut-être une sous-classe) plutôt que de modifier 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 elle doit être utilisée rarement et avec prudence.

40voto

silvinci Points 1736

Il n'y a pas d'inconvénient mesurable, comme une baisse de performance. Du moins, personne n'en a parlé. Il s'agit donc d'une question de préférence et d'expériences personnelles.

Le principal argument pro : Il a une meilleure apparence et est plus intuitif : 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 contre : Le code peut interférer. Si la librairie A ajoute une fonction, elle pourrait écraser la fonction de la librairie B. Cela peut casser le code très facilement.

Les deux ont raison. Lorsque vous vous appuyez sur deux bibliothèques qui changent directement vos types, vous finirez très probablement par avoir un code cassé car la fonctionnalité attendue n'est probablement pas la même. Je suis tout à fait d'accord sur ce point. 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 librairies comme jQuery, underscore, etc. Ne vous méprenez pas ; elles sont absolument bien programmées et fonctionnent comme un charme, mais elles sont grand . Vous n'en utilisez que 10 % et n'en comprenez qu'environ 1 %.

C'est pourquoi je préfère un approche atomistique où vous ne demandez que 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èrent 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, n'étendez pas 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. Sur pas de de manipuler 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 utiliser le type prototype .


J'ai eu l'idée de cette question parce que je voulais Error pour qu'ils puissent être envoyés via JSON. J'avais donc besoin d'un moyen de les stringifier. error.stringify() était bien mieux que errorlib.stringify(error) Comme la deuxième construction le suggère, j'opère sur errorlib et non sur error lui-même.

21voto

Stefan Points 2539

À mon avis, c'est une mauvaise pratique. La raison principale est l'intégration. Citation de la documentation de should.js :

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

Eh bien, comment l'auteur peut-il le savoir ? Et si mon framework de mocking fait la même chose ? Et si ma librairie de promesses fait la même chose ?

Si vous le faites dans votre propre projet, c'est parfait. Mais pour une bibliothèque, c'est une mauvaise conception. Underscore.js est un exemple de la chose faite de la bonne manière :

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

19voto

SoEzPz Points 71

Si vous examinez la situation au cas par cas, certaines mises en œuvre sont peut-être acceptables.

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

L'écrasement de méthodes déjà créées crée plus de problèmes qu'il n'en résout, c'est pourquoi il est généralement indiqué, dans de nombreux langages de programmation, d'éviter cette pratique. Comment les développeurs peuvent-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 ne remplaçons pas une méthode connue du noyau JS, mais nous étendons String. Un argument dans ce post mentionnait comment le nouveau développeur peut savoir si cette méthode fait partie du core JS, ou où trouver la documentation ? Que se passerait-il si l'objet String de Core JS obtenait une méthode nommée majuscules ?

Et si, au lieu d'ajouter des noms qui risquent d'entrer en conflit avec d'autres bibliothèques, vous utilisiez un modificateur spécifique à la société/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 continuez à étendre d'autres objets, tous les développeurs les rencontreront dans la nature avec (dans ce cas) nous qui les informerait qu'il s'agit d'une extension spécifique à une entreprise ou à une application.

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

11voto

L'extension des prototypes de built-ins est en effet une mauvaise idée. Cependant, l'ES2015 a introduit une nouvelle technique qui peut être utilisée pour obtenir le comportement souhaité :

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

L'implémentation suivante étend le Number y Array des prototypes sans les toucher :

// 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. Elle utilise une structure de carte et Object identité pour associer des types à des prototypes intégrés.

Mon exemple permet à un reduce qui n'attend qu'un Array en tant qu'argument unique, parce qu'il peut extraire l'information comment créer un accumulateur vide et comment concaténer des éléments avec cet accumulateur à partir des éléments du tableau lui-même.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X