La propiedad "prototype"
es ampliamente utilizada por el núcleo mismo de JavaScript. Todas las funciones de constructor integradas lo usan.
Primero veremos los detalles, y luego cómo usarlo para agregar nuevas capacidades a los objetos integrados.
Object.prototype
Digamos que tenemos un objeto vacío y lo mostramos:
let obj = {};
alert( obj ); // "[object Object]" ?
¿Dónde está el código que genera la cadena "[object Object]"
? Es un método nativo toString
, pero ¿dónde está? ¡El obj
está vacío!
…Pero la notación corta obj = {}
es la mismo que obj = new Object()
, donde Object
es una función de constructor de objeto integrado, 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 {...}
), el [[Prototype]]
se establece en Object.prototype
de acuerdo con la regla que discutimos en el capitulo anterior:
Entonces, cuando se llama a obj.toString()
, el método se toma de Object.prototype
.
Lo podemos comprobar así:
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
Tenga en cuenta que no hay más [[Prototype]]
en la cadena encima de Object.prototype
:
alert(Object.prototype.__proto__); // null
Otros prototipos integrados
Otros objetos integrados, como Array
, Date
, Function
, también mantienen los métodos en sus 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 sus métodos. Eso es muy eficiente en memoria.
Por especificación, todos los prototipos integrados tienen Object.prototype
en el tope. Es por eso que algunos dicen “todo hereda de los objetos”.
Aquí está la imagen general de 3 objetos integrados (3 para que quepan):
Verifiquemos los prototipos manualmente:
let arr = [1, 2, 3];
// se hereda de Array.prototype?
alert( arr.__proto__ === Array.prototype ); // verdadero
// y despues desde Object.prototype?
alert( arr.__proto__.__proto__ === Object.prototype ); // verdadero
// Y null en el tope.
alert( arr.__proto__.__proto__.__proto__ ); // null
Algunos métodos en prototipos pueden superponerse; por ejemplo, Array.prototype
tiene su propio toString
que enumera elementos delimitados por comas:
let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- el resultado de Array.prototype.toString
Como hemos visto antes, Object.prototype
también tiene toString
, pero en la cadena, Array.prototype
está más cerca, por lo que se utiliza la variante de array.
Las herramientas en el navegador, como la consola de desarrollador de Chrome, también muestran herencia (es posible que deba utilizarse console.dir
para los 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
.
function f() {}
alert(f.__proto__ == Function.prototype); // verdadero
alert(f.__proto__.__proto__ == Object.prototype); // verdadero, hereda de objetos
Primitivos
Lo más intrincado sucede con cadenas, números y booleanos.
Como recordamos, no son objetos. Pero si tratamos de acceder a sus propiedades, se crean los objetos contenedores temporales utilizando los constructores integrados String
, Number
y Boolean
, estos proporcionan los métodos y luego desaparecen.
Estos objetos se crean de manera 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
.
null
y undefined
no tienen objetos contenedoresLos valores especiales null
y undefined
se distinguen. No tienen objetos contenedores, por lo que los métodos y propiedades no están disponibles para ellos. Y tampoco tienen los prototipos correspondientes.
Cambiando prototipos nativos
Los prototipos nativos pueden ser modificados. Por ejemplo, si agregamos un método a String.prototype
, estará disponible para todas las cadenas:
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
Durante el proceso de desarrollo, podemos tener ideas para nuevos métodos integrados que nos gustaría tener, y podemos sentir la tentación de agregarlos a los prototipos nativos. Pero eso es generalmente una mala idea.
Los prototipos son globales, por lo que es fácil generar un conflicto. Si dos bibliotecas agregan un método String.prototype.show
, entonces una de ellas sobrescribirá el método de la otra.
Por lo tanto, en general, modificar un prototipo nativo se considera una mala idea.
En la programación moderna, solo hay un caso en el que se aprueba la modificación de prototipos nativos: haciendo un polyfill.
Cuando un método existe en la especificación de JavaScript, pero aún no está soportado por un motor de JavaScript en particular, podemos hacer “polyfill”; esto es, crear un método sustituto.
Luego podemos implementarlo manualmente y completar el prototipo integrado con él.
Por ejemplo:
if (!String.prototype.repeat) { // si no hay tal método
// agregarlo al prototipo
String.prototype.repeat = function(n) {
// repite 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 (polirelleno) imperfecto a menudo se considera lo suficientemente bueno
return new Array(n + 1).join(this);
};
}
alert( "La".repeat(3) ); // LaLaLa
Préstamo de prototipos
En el capítulo Decoradores y redirecciones, call/apply hablamos sobre el préstamo de método .
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 haciendo un objeto tipo array, es posible que queramos copiar algunos métodos de ‘Array’.
P. ej…
let obj = {
0: "Hola",
1: "mundo!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hola,mundo!
Funciona porque el algoritmo interno del método integrado join
solo se preocupa por los índices correctos y la propiedad length
. No comprueba si el objeto es realmente un arreglo. 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, solo podemos heredar de un objeto a la vez.
Los métodos de préstamo son flexibles, permiten mezclar funcionalidades de diferentes objetos si es necesario.
Resumen
- 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í solo almacena los datos (elementos de arreglo, propiedades de objeto, la fecha)
- Los métodos se almacenan en el prototipo (
- Los primitivos también almacenan métodos en prototipos de objetos contenedores:
Number.prototype
,String.prototype
yBoolean.prototype
. Soloundefined
ynull
no tienen objetos contenedores. - 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 que aún no es soportado por el motor de JavaScript.