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

136

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

Tomemos como ejemplo TJ Holowaychuk 's should.js . Se añade un simple comprador de Objecty 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 extenderse 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?

Buschtoens
fuente
9
¿Qué espera que suceda cuando, más adelante, se cambie un objeto nativo para incluir una función "eliminar" con una semántica diferente a la suya? No controlas el estándar.
ta.speot.is
1
No es tu tipo nativo. Es el tipo nativo de todos.
66
"¿Temen que alguien lo haga" de manera incorrecta "y agregue tipos enumerables a Object, prácticamente destruyendo todos los bucles de cualquier objeto?" : Sí. En los días en que se formó esta opinión, era imposible crear propiedades no enumerables. Ahora las cosas pueden ser diferentes a este respecto, pero imagine que cada biblioteca solo extiende los objetos nativos como lo desean. Hay una razón por la que comenzamos a usar espacios de nombres.
Felix Kling
55
Por lo que vale, algunos "líderes de opinión", por ejemplo Brendan Eich, piensan que está perfectamente bien extender los prototipos nativos.
Benjamin Gruenbaum
2
Foo no debería ser global en estos días, hemos incluido, requirejs, commonjs, etc.
Jamie Pate

Respuestas:

127

Cuando extiende un objeto, cambia su comportamiento.

Está bien cambiar el comportamiento de un objeto que solo será utilizado por su propio código. Pero cuando cambia el comportamiento de algo que también usa otro código, existe el riesgo de que rompa ese otro código.

Cuando se trata de agregar métodos a las clases de objetos y matrices 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 (quizás una subclase) en lugar de cambiar una nativa. De esa manera no romperás nada en absoluto.

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

Abhi Beckert
fuente
2
Entonces, ¿agregar algo como .stgringify()se consideraría seguro?
buschtoens
77
Hay muchos problemas, por ejemplo, ¿qué sucede si algún otro código también intenta agregar su propio stringify()método con un comportamiento diferente? Realmente no es algo que debas hacer en la programación diaria ... no si todo lo que quieres hacer es guardar algunos caracteres de código aquí y allá. Es mejor definir su propia clase o función que acepte cualquier entrada y la stringifique. La mayoría de los navegadores definen JSON.stringify(), lo mejor sería verificar si eso existe, y si no, definirlo usted mismo.
Abhi Beckert
55
Yo prefiero someError.stringify()más errors.stringify(someError). Es sencillo y se adapta perfectamente al concepto de js. Estoy haciendo algo que está específicamente vinculado a un cierto ErrorObject.
buschtoens
1
El único (pero bueno) argumento que queda es que algunas macro-bibliotecas enormes pueden comenzar a hacerse cargo de sus tipos y pueden interferir entre sí. ¿O hay algo más?
buschtoens
44
Si el problema es que podría tener una colisión, ¿podría usar un patrón en el que cada vez que hiciera esto, siempre confirme que la función no está definida antes de definirla? Y si siempre coloca bibliotecas de terceros antes de su propio código, debería poder detectar siempre tales colisiones.
Dave Cousineau
32

No hay inconveniente medible, como un éxito en el rendimiento. Al menos nadie mencionó ninguno. Así que esta es una cuestión de preferencia personal y experiencias.

El principal argumento pro: se ve mejor y es más intuitivo: sintaxis de azúcar. 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 romper el código muy fácilmente.

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

Y esa es la razón por la que no me gustan las librerías como jQuery, subrayado, etc. No me malinterpreten; están absolutamente bien programados y funcionan de maravilla, pero son grandes . Utiliza solo el 10% de ellos y comprende aproximadamente el 1%.

Es por eso que prefiero un enfoque atomista , donde solo necesita lo que realmente necesita. De esta manera, siempre sabes lo que sucede. Las microbibliotecas solo hacen lo que quieres que hagan, por lo que no interferirán. En el contexto de que el usuario final sepa qué características se agregan, la extensión de los tipos nativos puede considerarse segura.

TL; DR En caso de duda, no extienda los tipos nativos. Extienda solo un tipo nativo si está 100% seguro de que el usuario final sabrá y querrá ese comportamiento. En ningún caso manipule 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 los tipos prototype.


Originalmente se me ocurrió esta pregunta porque quería Errorque se enviaran mensajes vía JSON. Entonces, necesitaba una forma de encadenarlos. error.stringify()se sintió mucho mejor que errorlib.stringify(error); Como sugiere la segunda construcción, estoy operando errorliby no en errorsí mismo.

Buschtoens
fuente
Estoy abierto a otras opiniones sobre esto.
buschtoens
44
¿Estás insinuando que jQuery y el subrayado extienden objetos nativos? Ellos no. Entonces, si los estás evitando por esa razón, estás equivocado.
JLRishe
1
Aquí hay una pregunta que creo que se pierde en el argumento de que extender objetos nativos con lib A puede entrar en conflicto con lib B: ¿Por qué tendrías dos bibliotecas que son tan similares en naturaleza o son tan amplias en naturaleza que este conflicto es posible? Es decir, o elijo lodash o subrayar no ambos. Nuestro paisaje libraray actual en javascript está tan excesivamente saturado y desarrolladores (en general) a ser tan descuidados en adición a los que acabamos de evitar las prácticas óptimas para apaciguar nuestra Lib Lores
micahblu
2
@micahblu: una biblioteca podría decidir modificar un objeto estándar solo por la conveniencia de su propia programación interna (a menudo es por eso que la gente quiere hacer esto). Por lo tanto, este conflicto no se evita solo porque no estaría utilizando dos bibliotecas que tienen la misma función.
jfriend00
1
También puede (a partir de es2015) crear una nueva clase que amplíe una clase existente y usarla en su propio código. así que si MyError extends Errorpuede tener un stringifymétodo sin chocar con otras subclases. Todavía tiene que manejar los errores no generados por su propio código, por lo que puede ser menos útil Errorcon otros.
ShadSterling
16

En mi opinión, es una mala práctica. La razón principal es la integración. Citando los documentos de should.js:

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

Bueno, ¿cómo puede saber el autor? ¿Qué pasa si mi marco burlón hace lo mismo? ¿Qué pasa si mis promesas lib hacen lo mismo?

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

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Stefan
fuente
2
Estoy seguro de que la respuesta de TJ sería no usar esas promesas o marcos burlones: x
Jim Schubert
El _(arr).flatten()ejemplo realmente me convenció de no extender los objetos nativos. Mi razón personal para hacerlo fue puramente sintáctica. Pero esto satisface mi sensación estética :) Incluso usar algún nombre de función más regular como foo(native).coolStuff()convertirlo en algún objeto "extendido" se ve muy bien sintácticamente. ¡Gracias por eso!
egst
16

Si lo mira 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 dice comúnmente, en muchos lenguajes de programación, evitar esta práctica. ¿Cómo deben saber los desarrolladores que la función ha cambiado?

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 JS central conocido, pero estamos extendiendo String. Un argumento en esta publicación mencionó cómo debe saber el nuevo desarrollador si este método es parte del núcleo JS, o dónde encontrar los documentos. ¿Qué pasaría si el objeto JS String principal obtuviera un método llamado capitalizar ?

¿Qué sucede si en lugar de agregar nombres que pueden chocar con otras bibliotecas, usó un modificador específico de la empresa / aplicación que todos los desarrolladores podrían 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 continuabas extendiendo otros objetos, todos los desarrolladores los encontrarían en la naturaleza con nosotros (en este caso) , lo que les notificaría que era 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 los objetos centrales de JS es para usted y / o su equipo, quizás esto sea para usted.

SoEzPz
fuente
7

Extender prototipos de empotrados es, de hecho, una mala idea. Sin embargo, ES2015 introdujo una nueva técnica que puede utilizarse para obtener el comportamiento deseado:

Utilizando WeakMaps para asociar tipos con prototipos incorporados

La siguiente implementación extiende los prototipos Numbery Arraysin 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 pueden extenderse mediante esta técnica. Utiliza una estructura de mapa e Objectidentidad para asociar tipos con prototipos incorporados.

Mi ejemplo habilita una reducefunción que solo espera un Arraycomo argumento único, porque puede extraer la información de cómo crear un acumulador vacío y cómo concatenar elementos con este acumulador a partir de los elementos de la matriz.

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


fuente
Este es un uso genial de WeakMap. Tenga cuidado con eso xs[0]en matrices vacías aunque. type(undefined).monoid ...
Gracias
@naomik Lo sé: vea mi última pregunta sobre el segundo fragmento de código.
¿Qué tan estándar es el soporte para este @ftor?
onassar
55
-1; ¿Cuál es el punto de nada de esto? Simplemente está creando un tipo de mapeo de diccionario global separado para los métodos, y luego está buscando explícitamente los métodos en ese diccionario por tipo. Como consecuencia, pierde el soporte para la herencia (un método "agregado" de Object.prototypeesta manera no se puede invocar en un Array), y la sintaxis es significativamente más larga / fea que si realmente extendiera un prototipo. Casi siempre sería más simple crear clases de utilidad con métodos estáticos; La única ventaja que tiene este enfoque es un soporte limitado para el polimorfismo.
Mark Amery
44
@ MarkAmery Hola amigo, no lo entiendes. No pierdo herencia, pero me deshago de ella. La herencia es tan de los 80 que deberías olvidarte de ella. El objetivo de este truco es imitar las clases de tipos, que, simplemente, son funciones sobrecargadas. Sin embargo, hay una crítica legítima: ¿es útil imitar clases de tipos en Javascript? No lo es. En su lugar, use un lenguaje escrito que los admita de forma nativa. Estoy bastante seguro de que esto es lo que has querido decir.
7

Una razón más por la que debe no extender nativos objetos:

Usamos Magento que usa prototype.js y extiende muchas cosas en objetos nativos. Esto funciona bien hasta que decida obtener nuevas funciones y ahí es donde comienzan los grandes problemas.

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

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

Eugen Wesseloh
fuente
4

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

  1. Si lo hace mal, accidentalmente agregará una propiedad enumerable a todos los objetos del tipo extendido. Se solucionó fácilmente usando Object.defineProperty, lo que crea propiedades no enumerables de forma predeterminada.
  2. Puede causar un conflicto con una biblioteca que está utilizando. Se puede evitar con diligencia; solo verifique qué métodos definen las bibliotecas que usa antes de agregar algo a un prototipo, verifique las notas de la versión al actualizar y pruebe su aplicación.
  3. Podría causar un conflicto con una versión futura del entorno nativo de JavaScript.

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

(Vea también la historia de cómo los mootools que manipulaban prototipos que no poseían obligaron a cambiar el nombre de un método ES6 para evitar romper la red ).

Esto se puede evitar mediante el uso de un prefijo específico de la aplicación para métodos agregados a objetos nativos (por ejemplo, definir en Array.prototype.myappSwizzlelugar de Array.prototype.swizzle), pero eso es algo feo; es igualmente solucionable mediante el uso de funciones de utilidad independientes en lugar de aumentar los prototipos.

Mark Amery
fuente
2

Perf también es una razón. A veces es posible que deba recorrer las teclas. Hay varias formas de hacer esto

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

Usualmente lo uso for of Object.keys()porque hace lo correcto y es relativamente breve, no es necesario agregar el cheque.

Pero es mucho más lento .

resultados de rendimiento for-of vs for-in

Solo adivinar que la razón Object.keyses lenta es obvio, Object.keys()tiene que hacer una asignación. De hecho, AFAIK tiene que asignar una copia de todas las claves desde entonces.

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

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

Es posible que el motor JS pueda usar algún tipo de estructura de clave inmutable para que Object.keys(object)devuelva una referencia que repita algo sobre las claves inmutables y que object.newPropcree un objeto de claves inmutable completamente nuevo, pero lo que sea, es claramente más lento hasta 15x

Incluso la comprobación hasOwnPropertyes hasta 2 veces más lenta.

El punto de todo eso es que si tiene un código sensible al rendimiento y necesita recorrer las teclas, entonces desea poder usar for insin tener que llamar hasOwnProperty. Solo puede hacer esto si no ha modificadoObject.prototype

tenga en cuenta que si utiliza Object.definePropertypara modificar el prototipo si las cosas que agrega no son enumerables, no afectarán el comportamiento de JavaScript en los casos anteriores. Desafortunadamente, al menos en Chrome 83, sí afectan el rendimiento.

ingrese la descripción de la imagen aquí

Agregué 3000 propiedades no enumerables solo para intentar forzar la aparición de cualquier problema de rendimiento. Con solo 30 propiedades, las pruebas fueron demasiado cercanas para determinar si hubo algún impacto en el rendimiento.

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

Firefox 77 y Safari 13.1 no mostraron diferencias en el rendimiento entre las clases aumentadas y no aumentadas, tal vez v8 se solucionará en esta área y puede ignorar los problemas de rendimiento.

Pero, permítanme agregar también que hay una historia deArray.prototype.smoosh . La versión corta es Mootools, una biblioteca popular, hecha propia Array.prototype.flatten. Cuando el comité de estándares trató de agregar un nativo Array.prototype.flatten, descubrieron que no podían sin romper muchos sitios. Los desarrolladores que se enteraron del descanso sugirieron nombrar el método es5 smooshcomo una broma, pero la gente se asustó al no entender que era una broma. Se establecieron en flatlugar deflatten

La moraleja de la historia es que no debes extender los objetos nativos. Si lo hace, podría encontrarse con el mismo problema de ruptura de sus cosas y, a menos que su biblioteca en particular sea tan popular como MooTools, es poco probable que los proveedores del navegador solucionen el problema que causó. Si su biblioteca se vuelve tan popular, sería un poco malo obligar a todos los demás a solucionar el problema que causó. Entonces, por favor no extienda los objetos nativos

gman
fuente
Espera, hasOwnPropertyte dice si la propiedad está en el objeto o en el prototipo. Pero el for inciclo solo te da propiedades enumerables. Acabo de probar esto: agregue una propiedad no enumerable al prototipo de matriz y no se mostrará en el bucle. No es necesario no modificar el prototipo para que el bucle más simple funcione.
ygoe
Pero sigue siendo una mala práctica por otras razones;)
Gman