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 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 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?
javascript
prototype
prototypal-inheritance
Buschtoens
fuente
fuente
Respuestas:
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.
fuente
.stgringify()
se consideraría seguro?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 definenJSON.stringify()
, lo mejor sería verificar si eso existe, y si no, definirlo usted mismo.someError.stringify()
máserrors.stringify(someError)
. Es sencillo y se adapta perfectamente al concepto de js. Estoy haciendo algo que está específicamente vinculado a un cierto ErrorObject.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 tiposprototype
.Originalmente se me ocurrió esta pregunta porque quería
Error
que se enviaran mensajes vía JSON. Entonces, necesitaba una forma de encadenarlos.error.stringify()
se sintió mucho mejor queerrorlib.stringify(error)
; Como sugiere la segunda construcción, estoy operandoerrorlib
y no enerror
sí mismo.fuente
MyError extends Error
puede tener unstringify
mé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 útilError
con otros.En mi opinión, es una mala práctica. La razón principal es la integración. Citando los documentos de should.js:
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:
fuente
_(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 comofoo(native).coolStuff()
convertirlo en algún objeto "extendido" se ve muy bien sintácticamente. ¡Gracias por eso!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?
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?
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.
fuente
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
WeakMap
s para asociar tipos con prototipos incorporadosLa siguiente implementación extiende los prototipos
Number
yArray
sin tocarlos en absoluto:Como puede ver, incluso los prototipos primitivos pueden extenderse mediante esta técnica. Utiliza una estructura de mapa e
Object
identidad para asociar tipos con prototipos incorporados.Mi ejemplo habilita una
reduce
función que solo espera unArray
como 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
Map
tipo normal , ya que las referencias débiles no tienen sentido cuando simplemente representan prototipos incorporados, que nunca se recolectan basura. Sin embargo, aWeakMap
no 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
xs[0]
en matrices vacías aunque.type(undefined).monoid ...
Object.prototype
esta manera no se puede invocar en unArray
), 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.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!
fuente
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í:
Object.defineProperty
, lo que crea propiedades no enumerables de forma predeterminada.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 agregaArray.prototype.swizzle(bar, foo)
a Chrome, es probable que termine con algunos colegas confundidos que se preguntan por qué.swizzle
el 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.myappSwizzle
lugar deArray.prototype.swizzle
), pero eso es algo feo; es igualmente solucionable mediante el uso de funciones de utilidad independientes en lugar de aumentar los prototipos.fuente
Perf también es una razón. A veces es posible que deba recorrer las teclas. Hay varias formas de hacer esto
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 .
Solo adivinar que la razón
Object.keys
es 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.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 queobject.newProp
cree un objeto de claves inmutable completamente nuevo, pero lo que sea, es claramente más lento hasta 15xIncluso la comprobación
hasOwnProperty
es 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 in
sin tener que llamarhasOwnProperty
. Solo puede hacer esto si no ha modificadoObject.prototype
tenga en cuenta que si utiliza
Object.defineProperty
para 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.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 de
Array.prototype.smoosh
. La versión corta es Mootools, una biblioteca popular, hecha propiaArray.prototype.flatten
. Cuando el comité de estándares trató de agregar un nativoArray.prototype.flatten
, descubrieron que no podían sin romper muchos sitios. Los desarrolladores que se enteraron del descanso sugirieron nombrar el método es5smoosh
como una broma, pero la gente se asustó al no entender que era una broma. Se establecieron enflat
lugar 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
fuente
hasOwnProperty
te dice si la propiedad está en el objeto o en el prototipo. Pero elfor in
ciclo 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.