¿Por qué extender objetos nativos es una mala práctica?


Cada líder de opinión de JS dice que extender los objetos nativos es una mala práctica. ¿Pero por qué? ¿Tenemos un golpe de rendimiento? ¿Temen que alguien lo haga "de manera incorrecta", y agregue tipos enumerables a Object, prácticamente destruyendo todos los bucles en cualquier objeto?

Take TJ Holowaychuk's should.js por ejemplo. Él añade un simple getter a Object y todo funciona bien (fuente).

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

Esto realmente tiene sentido. Por ejemplo, uno podría extender Array.

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

¿Hay algún argumento en contra de extender los tipos nativos?

Author: giampaolo, 2012-12-26

7 answers

Cuando extiendes un objeto, cambias su comportamiento.

Cambiar el comportamiento de un objeto que solo será utilizado por su propio código está bien. Pero cuando cambias el comportamiento de algo que también es usado por otro código, existe el riesgo de que rompas ese otro código.

Cuando se trata de agregar métodos a las clases object y array en javascript, el riesgo de romper algo es muy alto, debido a cómo funciona javascript. Largos años de experiencia me han enseñado que este tipo de cosas causa todo tipo de errores terribles en javascript.

Si necesita un comportamiento personalizado, es mucho mejor definir su propia clase (tal vez una subclase) en lugar de cambiar una nativa. De esa manera no romperás nada en absoluto.

La capacidad de cambiar cómo funciona una clase sin subclasificarla es una característica importante de cualquier buen lenguaje de programación, pero es uno que debe usarse raramente y con precaución.

 93
Author: Abhi Beckert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-25 22:02:38

No hay inconveniente mensurable, como un éxito de rendimiento. Al menos nadie mencionó ninguno. Así que esta es una cuestión de preferencias personales y experiencias.

El principal argumento pro: Se ve mejor y es más intuitivo: syntax sugar. Es una función específica de tipo / instancia, por lo que debe estar específicamente vinculada a ese tipo/instancia.

El principal argumento en contra: El código puede interferir. Si lib A agrega una función, podría sobrescribir la función de lib B. Esto puede rompa el código muy fácilmente.

, Ambos tienen un punto. Cuando confías en dos bibliotecas que cambian directamente tus tipos, lo más probable es que termines con código roto, ya que la funcionalidad esperada probablemente no sea la misma. Estoy totalmente de acuerdo en eso. Las macro-bibliotecas no deben manipular los tipos nativos. De lo contrario, usted como desarrollador nunca sabrá lo que realmente está sucediendo detrás de escena.

Y esa es la razón por la que no me gustan las bibliotecas como jQuery, underscore, etc. No me malinterpretes; son absolutamente bien programados y funcionan como un encanto, pero son grandes . Usted usa solo el 10% de ellos, y entiende alrededor del 1%.

Es por eso que prefiero un enfoque atomístico, donde solo se requiere lo que realmente se necesita. De esta manera, siempre sabes lo que pasa. Las micro-bibliotecas solo hacen lo que usted quiere que hagan, por lo que no interferirán. En el contexto de que el usuario final sepa qué características se agregan, se puede considerar extender los tipos nativos seguro.

TL; DR En caso de duda, no extienda los tipos nativos. Solo extienda un tipo nativo si está 100% seguro de que el usuario final sabrá y querrá ese comportamiento. En ningún caso manipular las funciones existentes de un tipo nativo, ya que rompería la interfaz existente.

Si decide extender el tipo, use Object.defineProperty(obj, prop, desc); si no puedes , usa el tipo prototype.


Originalmente se me ocurrió esta pregunta porque quería Error s para ser se puede enviar a través de JSON. Por lo tanto, necesitaba una manera de stringify ellos. error.stringify() se sintió mucho mejor que errorlib.stringify(error); como sugiere el segundo constructo, estoy operando en errorlib y no en error en sí mismo.

 27
Author: buschtoens,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-19 15:30:38

En mi opinión, es una mala práctica. La razón principal es la integración. Citar debería.js docs:

OMG SE EXTIENDE OBJETO???!?!@ Sí, sí lo hace, con un solo getter debería, y no, no romperá su código

Bueno, ¿cómo puede saberlo el autor? ¿Qué pasa si mi marco de burlarse hace lo mismo? ¿Qué pasa si mis promesas lib hace lo mismo?

Si lo estás haciendo en tu propio proyecto, entonces está bien. Pero para una biblioteca, entonces es un mal diseño. Subrayar.js es un ejemplo de la cosa hecha de la manera correcta:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
 11
Author: Stefan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-25 22:26:09

Si lo miras caso por caso, quizás algunas implementaciones sean aceptables.

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

Sobrescribir los métodos ya creados crea más problemas de los que resuelve, por lo que se afirma comúnmente, en muchos lenguajes de programación, para evitar esta práctica. ¿Cómo saben los desarrolladores que la función ha sido cambiada?

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

En este caso no estamos sobrescribiendo ningún método core JS conocido, sino que estamos extendiendo String. Un argumento en este post mencionó cómo es el nuevo desarrollador para saber si este método es parte del núcleo JS, o dónde encontrar los documentos? ¿Qué pasaría si el objeto core JS String obtuviera un método llamado capitalizar?

¿Qué pasaría si en lugar de agregar nombres que podrían colisionar con otras bibliotecas, utilizaras un modificador específico de la empresa/aplicación que todos los desarrolladores pudieran entender?

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 continuaras extendiendo otros objetos, todos los desarrolladores se encontrarían con ellos en la naturaleza con (en este caso) we , que sería notifíqueles que se trataba de una extensión específica de la empresa / aplicación.

Esto no elimina las colisiones de nombres, pero reduce la posibilidad. Si determina que extender objetos core JS es para usted y / o su equipo, tal vez esto sea para usted.

 8
Author: SoEzPz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-18 15:21:13

Extender prototipos de built-ins es de hecho una mala idea. Sin embargo, ES2015 introdujo una nueva técnica que se puede utilizar para obtener el comportamiento deseado:

Utilizando WeakMaps para asociar tipos con prototipos incorporados

La siguiente implementación extiende los prototipos Number y Array sin tocarlos en absoluto:

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

Como puede ver, incluso los prototipos primitivos se pueden extender con esta técnica. Utiliza una estructura de mapa y Object identidad para asociar tipos con prototipos incorporados.

Mi ejemplo habilita una función reduce que solo espera un Array como su único argumento, porque puede extraer la información de cómo crear un acumulador vacío y cómo concatenar elementos con este acumulador de los elementos del propio Array.

Tenga en cuenta que podría haber utilizado el tipo normal Map, ya que las referencias débiles no tienen sentido cuando simplemente representan prototipos incorporados, que nunca se recoge basura. Sin embargo, un WeakMap no es iterable y no puede ser inspeccionado a menos que tenga la clave correcta. Esta es una característica deseada, ya que quiero evitar cualquier forma de reflexión de tipo.

 6
Author: ftor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-01 16:09:38

Una razón más por la que debería no extender Objetos nativos:

Utilizamos Magento que utiliza prototipo.js y extiende un montón de cosas sobre Objetos nativos. Esto funciona bien hasta que decida obtener nuevas características y ahí es donde comienzan los grandes problemas.

Hemos introducido Webcomponents en una de nuestras páginas, por lo que el webcomponents-lite.js decide reemplazar todo el objeto de Evento (nativo) en IE (¿por qué?). Esto, por supuesto, rompe prototipo.js que a su vez se rompe Magento. (hasta que encuentre el problema, puede invertir muchas horas rastreándolo)

Si te gustan los problemas, ¡sigue haciéndolo!

 2
Author: Eugen Wesseloh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-02 11:51:27

Puedo ver tres razones para no hacer esto (desde dentro de una aplicación , al menos), solo dos de las cuales se abordan en las respuestas existentes aquí:

  1. Si lo hace mal, añadirá accidentalmente una propiedad enumerable a todos los objetos del tipo extendido. Fácilmente trabajó alrededor usando Object.defineProperty, que crea propiedades no enumerables por defecto.
  2. Puede causar un conflicto con una biblioteca que está utilizando. Se puede evitar con diligencia; solo comprobar lo que métodos las bibliotecas que utiliza definen antes de añadir algo a un prototipo, compruebe las notas de la versión al actualizar y pruebe su aplicación.
  3. Puede causar un conflicto con una versión futura del entorno JavaScript nativo.

El punto 3 es posiblemente el más importante. Puede asegurarse, a través de pruebas, de que las extensiones de sus prototipos no causen ningún conflicto con las bibliotecas que usa, porque usted decide qué bibliotecas usa. Lo mismo no es cierto de objetos nativos, suponiendo que su código se ejecuta en un navegador. Si se define Array.prototype.swizzle(foo, bar) hoy, y mañana Google añade Array.prototype.swizzle(bar, foo) a Chrome, usted es probable que termine con algunos colegas confundidos que se preguntan por qué .swizzle's comportamiento no parece coincidir con lo que está documentado en MDN.

(Véase también la historia de de cómo el jugueteo de mootools con prototipos que no poseían obligó a renombrar un método ES6 para evitar romper la web.)

Esto es evitable mediante el uso de un prefijo específico de la aplicación para los métodos agregados a objetos nativos (por ejemplo, define Array.prototype.myappSwizzle en lugar de Array.prototype.swizzle), pero eso es un poco feo; es igual de fácil de resolver mediante el uso de funciones de utilidad independientes en lugar de aumentar los prototipos.

 0
Author: Mark Amery,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-29 19:32:37