La propiedad "prototype"
es ampliamente utilizada por el propio núcleo de JavaScript. Todas las funciones de constructor integradas lo utilizan.
Primero veremos los detalles y luego cómo usarlo para agregar nuevas capacidades a los objetos integrados.
Digamos que generamos un objeto vacío:
dejar objeto = {}; alerta (obj); // "[objeto Objeto]" ?
¿Dónde está el código que genera la cadena "[object Object]"
? Ese es un método toString
integrado, pero ¿dónde está? ¡El obj
está vacío!
…Pero la notación corta obj = {}
es la misma que obj = new Object()
, donde Object
es una función constructora de objetos incorporada, con su propio prototype
que hace referencia a un objeto enorme con toString
y otros métodos.
Esto es lo que está pasando:
Cuando se llama a new Object()
(o se crea un objeto literal {...}
), su [[Prototype]]
se establece en Object.prototype
de acuerdo con la regla que analizamos en el capítulo anterior:
Entonces, cuando se llama obj.toString()
el método se toma de Object.prototype
.
Podemos comprobarlo así:
dejar objeto = {}; alerta(obj.__proto__ === Objeto.prototipo); // verdadero alerta(obj.toString === obj.__proto__.toString); //verdadero alerta(obj.toString === Object.prototype.toString); //verdadero
Tenga en cuenta que no hay más [[Prototype]]
en la cadena anterior Object.prototype
:
alerta(Objeto.prototipo.__proto__); // nulo
Otros objetos integrados como Array
, Date
, Function
y otros también mantienen métodos en prototipos.
Por ejemplo, cuando creamos una matriz [1, 2, 3]
, el constructor predeterminado new Array()
se usa internamente. Entonces Array.prototype
se convierte en su prototipo y proporciona métodos. Eso es muy eficiente en memoria.
Por especificación, todos los prototipos integrados tienen Object.prototype
en la parte superior. Por eso hay quien dice que “todo hereda de los objetos”.
Aquí está la imagen general (para que quepan 3 integrados):
Comprobemos los prototipos manualmente:
sea arr = [1, 2, 3]; // hereda de Array.prototype? alerta (arr.__proto__ === Array.prototype); // verdadero // luego de Object.prototype? alerta (arr.__proto__.__proto__ === Objeto.prototipo); // verdadero // y nulo en la parte superior. alerta (arr.__proto__.__proto__.__proto__); // nulo
Algunos métodos en prototipos pueden superponerse, por ejemplo, Array.prototype
tiene su propio toString
que enumera elementos delimitados por comas:
sea arr = [1, 2, 3] alerta(arr); // 1,2,3 <-- el resultado de Array.prototype.toString
Como hemos visto antes, Object.prototype
también tiene toString
, pero Array.prototype
está más cerca en la cadena, por lo que se usa la variante de matriz.
Las herramientas del navegador, como la consola para desarrolladores de Chrome, también muestran la herencia (es posible que sea necesario utilizar console.dir
para objetos integrados):
Otros objetos integrados también funcionan de la misma manera. Incluso las funciones son objetos de un constructor Function
integrado y sus métodos ( call
/ apply
y otros) se toman de Function.prototype
. Las funciones también tienen su propio toString
.
función f() {} alerta(f.__proto__ == Función.prototipo); // verdadero alerta(f.__proto__.__proto__ == Objeto.prototipo); // verdadero, hereda de objetos
Lo más complejo sucede con cadenas, números y valores booleanos.
Como recordamos, no son objetos. Pero si intentamos acceder a sus propiedades, los objetos contenedores temporales se crean utilizando los constructores integrados String
, Number
y Boolean
. Proporcionan los métodos y desaparecen.
Estos objetos se crean de forma invisible para nosotros y la mayoría de los motores los optimizan, pero la especificación lo describe exactamente de esta manera. Los métodos de estos objetos también residen en prototipos, disponibles como String.prototype
, Number.prototype
y Boolean.prototype
.
Los valores null
e undefined
no tienen envoltorios de objetos.
Los valores especiales null
e undefined
se distinguen. No tienen envoltorios de objetos, por lo que los métodos y propiedades no están disponibles para ellos. Y tampoco existen prototipos correspondientes.
Los prototipos nativos se pueden modificar. Por ejemplo, si agregamos un método a String.prototype
, estará disponible para todas las cadenas:
String.prototype.show = función() { alerta(esto); }; "¡BOOM!".mostrar(); // ¡BUM!
Durante el proceso de desarrollo, es posible que tengamos ideas para nuevos métodos integrados que nos gustaría tener y podemos sentirnos tentados a agregarlos a prototipos nativos. Pero en general esa es una mala idea.
Importante:
Los prototipos son globales, por lo que es fácil generar conflictos. Si dos bibliotecas agregan un método String.prototype.show
, una de ellas sobrescribirá el método de la otra.
Entonces, generalmente, modificar un prototipo nativo se considera una mala idea.
En la programación moderna, sólo hay un caso en el que se aprueba la modificación de prototipos nativos. Eso es polirrelleno.
Polyfilling es un término para sustituir un método que existe en la especificación de JavaScript, pero que aún no es compatible con un motor de JavaScript en particular.
Luego podemos implementarlo manualmente y completar el prototipo integrado con él.
Por ejemplo:
if (!String.prototype.repeat) { // si no existe tal método // agregarlo al prototipo Cadena.prototipo.repetir = función(n) { //repetir la cadena n veces // en realidad, el código debería ser un poco más complejo que eso // (el algoritmo completo está en la especificación) // pero incluso un polyfill imperfecto a menudo se considera suficientemente bueno devolver nueva matriz (n + 1). unirse (esto); }; } alerta( "La".repeat(3) ); // LaLaLa
En el capítulo Decoradores y reenvío, llamada/aplicación hablamos sobre el préstamo de métodos.
Ahí es cuando tomamos un método de un objeto y lo copiamos en otro.
A menudo se toman prestados algunos métodos de prototipos nativos.
Por ejemplo, si estamos creando un objeto similar a una matriz, es posible que deseemos copiarle algunos métodos Array
.
P.ej
dejar objeto = { 0: "Hola", 1: "¡mundo!", longitud: 2, }; obj.join = Array.prototype.join; alerta (obj.join(',')); // ¡Hola Mundo!
Funciona porque el algoritmo interno del método de join
integrado solo se preocupa por los índices correctos y la propiedad length
. No comprueba si el objeto es realmente una matriz. Muchos métodos integrados son así.
Otra posibilidad es heredar estableciendo obj.__proto__
en Array.prototype
, de modo que todos los métodos Array
estén disponibles automáticamente en obj
.
Pero eso es imposible si obj
ya hereda de otro objeto. Recuerde, sólo podemos heredar de un objeto a la vez.
Los métodos de préstamo son flexibles y permiten mezclar funcionalidades de diferentes objetos si es necesario.
Todos los objetos integrados siguen el mismo patrón:
Los métodos se almacenan en el prototipo ( Array.prototype
, Object.prototype
, Date.prototype
, etc.)
El objeto en sí almacena solo los datos (elementos de la matriz, propiedades del objeto, la fecha)
Las primitivas también almacenan métodos en prototipos de objetos contenedores: Number.prototype
, String.prototype
y Boolean.prototype
. Sólo undefined
y null
no tienen objetos contenedor
Los prototipos integrados se pueden modificar o completar con nuevos métodos. Pero no se recomienda cambiarlos. El único caso permitido es probablemente cuando agregamos un nuevo estándar, pero aún no es compatible con el motor JavaScript.
importancia: 5
Agregue al prototipo de todas las funciones el método defer(ms)
, que ejecuta la función después de ms
milisegundos.
Después de hacerlo, dicho código debería funcionar:
función f() { alerta("¡Hola!"); } f.diferir(1000); // muestra "¡Hola!" después de 1 segundo
Función.prototipo.defer = función(ms) { setTimeout(esto, ms); }; función f() { alerta("¡Hola!"); } f.diferir(1000); // muestra "¡Hola!" después de 1 segundo
importancia: 4
Agregue al prototipo de todas las funciones el método defer(ms)
, que devuelve un contenedor, retrasando la llamada en ms
milisegundos.
A continuación se muestra un ejemplo de cómo debería funcionar:
función f(a, b) { alerta (a + b); } f.defer(1000)(1, 2); // muestra 3 después de 1 segundo
Tenga en cuenta que los argumentos deben pasarse a la función original.
Función.prototipo.defer = función(ms) { sea f = esto; función de retorno (... argumentos) { setTimeout(() => f.apply(this, args), ms); } }; // compruébalo función f(a, b) { alerta (a + b); } f.defer(1000)(1, 2); // muestra 3 después de 1 segundo
Tenga en cuenta: usamos this
en f.apply
para que nuestra decoración funcione para métodos de objetos.
Entonces, si la función contenedora se llama como un método de objeto, entonces this
pasa al método original f
.
Función.prototipo.defer = función(ms) { sea f = esto; función de retorno (... argumentos) { setTimeout(() => f.apply(this, args), ms); } }; dejar usuario = { nombre: "Juan", decir Hola() { alerta(este.nombre); } } usuario.decirHola = usuario.decirHola.defer(1000); usuario.sayHola();