23 de septiembre de 2022

F.prototype

Recuerde: se pueden crear nuevos objetos con una función constructora, como new F().

Si F.prototype es un objeto, entonces el operador new lo usa para establecerlo como [[Prototype]] en el nuevo objeto.

Por favor tome nota:

JavaScript tiene herencia prototípica desde sus comienzos. Era una de las características principales del lenguaje.

Pero en los viejos tiempos no había acceso directo a ella. Lo único que funcionaba de manera confiable era una propiedad "prototype" de la función constructora, la que describimos en este capítulo. Por ello hay muchos scripts que todavía lo usan.

Tenga en cuenta que F.prototype aquí significa una propiedad regular llamada "prototype" en F. Suena algo similar al término “prototype”, pero aquí realmente queremos decir una propiedad regular con este nombre.

Aquí está el ejemplo:

let animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("Conejo Blanco"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // verdadero

La configuración de Rabbit.prototype = animal literalmente establece lo siguiente: "Cuando se crea un new Rabbit, asigne animal a su [[Prototype]].

Esta es la imagen resultante:

En la imagen, "prototype" es una flecha horizontal, que significa una propiedad regular, y [[Prototype]] es vertical, que significa la herencia de rabbit desde animal.

F.prototype solo se usa en el momento new F

La propiedad F.prototype solo se usa cuando se llama a new F: asigna [[Prototype]] del nuevo objeto.

Si, después de la creación, la propiedad F.prototype cambia (F.prototype = <otro objeto>), los nuevos objetos creados por new F tendrán otro objeto como [[Prototype]], pero los objetos ya existentes conservarán el antiguo.

F.prototype predeterminado, propiedad del constructor

Toda función tiene la propiedad "prototype" incluso si no la suministramos.

El "prototype" predeterminado es un objeto con la única propiedad constructor que apunta de nuevo a la función misma.

Como esto:

function Rabbit() {}

/* prototipo predeterminado
Rabbit.prototype = { constructor: Rabbit };
*/

Lo podemos comprobar:

function Rabbit() {}
// por defecto:
// Rabbit.prototype = { constructor: Rabbit }

alert( Rabbit.prototype.constructor == Rabbit ); // verdadero

Naturalmente, si no hacemos nada, la propiedad constructor está disponible para todos los rabbits a través de [[Prototype]]:

function Rabbit() {}
// por defecto:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // hereda de {constructor: Rabbit}

alert(rabbit.constructor == Rabbit); // verdadero (desde prototype)

Podemos usar la propiedad constructor para crear un nuevo objeto usando el constructor ya existente.

Como aqui:

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("Conejo Blanco");

let rabbit2 = new rabbit.constructor("Conejo Negro");

Eso es útil cuando tenemos un objeto, no sabemos qué constructor se usó para él (por ejemplo, proviene de una biblioteca de terceros), y necesitamos crear otro del mismo tipo.

Pero probablemente lo más importante sobre "constructor" es que …

…JavaScript en sí mismo no garantiza el valor correcto de "constructor".

Sí, existe en el "prototipo" predeterminado para las funciones, pero eso es todo. Lo que sucede con eso más tarde, depende totalmente de nosotros.

En particular, si reemplazamos el prototipo predeterminado como un todo, entonces no habrá "constructor" en él.

Por ejemplo:

function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};

let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // falso

Entonces, para mantener el "constructor" correcto, podemos elegir agregar/eliminar propiedades al "prototipo" predeterminado en lugar de sobrescribirlo como un todo:

function Rabbit() {}

// No sobrescribir totalmente Rabbit.prototype
// solo agrégale
Rabbit.prototype.jumps = true
// se conserva el Rabbit.prototype.constructor predeterminado

O, alternativamente, volver a crear la propiedad constructor manualmente:

Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

// ahora el constructor también es correcto, porque lo agregamos

Resumen

En este capítulo describimos brevemente la forma de establecer un [[Prototype]] para los objetos creados a través de una función de constructor. Más adelante veremos patrones de programación más avanzados que dependen de él.

Todo es bastante simple, solo algunas notas para aclarar las cosas:

  • La propiedad F.prototype (no la confunda con [[Prototype]]) establece [[Prototype]] de objetos nuevos cuando se llama a new F().
  • El valor de F.prototype debe ser: un objeto, o null. Otros valores no funcionarán.
  • La propiedad "prototype" solo tiene este efecto especial cuando se establece en una función de constructor y se invoca con new.

En los objetos normales, el prototype no es nada especial:

let user = {
  name: "John",
  prototype: "Bla-bla" // sin magia en absoluto
};

Por defecto, todas las funciones tienen F.prototype = {constructor: F}, por lo que podemos obtener el constructor de un objeto accediendo a su propiedad "constructor".

Tareas

importancia: 5

En el siguiente código creamos new Rabbit, y luego intentamos modificar su prototipo.

Al principio, tenemos este código:

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // verdadero
  1. Agregamos una cadena más (enfatizada). ¿Qué mostrará alert ahora?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = {};
    
    alert( rabbit.eats ); // ?
  2. …¿Y si el código es así (se reemplazó una línea)?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false;
    
    alert( rabbit.eats ); // ?
  3. ¿Y así (se reemplazó una línea)?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete rabbit.eats;
    
    alert( rabbit.eats ); // ?
  4. La última variante:

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete Rabbit.prototype.eats;
    
    alert( rabbit.eats ); // ?

Respuestas:

  1. verdadero.

    La asignación a Rabbit.prototype configura [[Prototype]] para objetos nuevos, pero no afecta a los existentes.

  2. falso.

    Los objetos se asignan por referencia. El objeto de Rabbit.prototype no está duplicado, sigue siendo un solo objeto referenciado tanto por Rabbit.prototype como por el [[Prototype]] de rabbit.

    Entonces, cuando cambiamos su contenido a través de una referencia, es visible a través de la otra.

  3. verdadero.

    Todas las operaciones delete se aplican directamente al objeto. Aquí delete rabbit.eats intenta eliminar la propiedad eats de rabbit, pero no la tiene. Entonces la operación no tendrá ningún efecto.

  4. undefined.

    La propiedad eats se elimina del prototipo, ya no existe.

importancia: 5

Imagínese, tenemos un objeto arbitrario obj, creado por una función constructora; no sabemos cuál, pero nos gustaría crear un nuevo objeto con él.

¿Podemos hacerlo así?

let obj2 = new obj.constructor();

Dé un ejemplo de una función constructora para obj que permita que dicho código funcione correctamente. Y un ejemplo que hace que funcione mal.

Podemos usar dicho enfoque si estamos seguros de que la propiedad "constructor" tiene el valor correcto.

Por ejemplo, si no tocamos el "prototype" predeterminado, con seguridad el código funciona:

function User(name) {
  this.name = name;
}

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // Pete (funcionó!)

Funcionó, porque User.prototype.constructor == User

… pero si alguien, por así decirlo, sobrescribiera User.prototype y olvidara recrear constructor para hacer referencia a User, entonces fallaría.

Por ejemplo:

function User(name) {
  this.name = name;
}
User.prototype = {}; // (*)

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // undefined

¿Por qué user2.name es undefined?

Así es como funciona new user.constructor('Pete'):

  1. Primero, busca a constructor en user. Nada.
  2. Sigue la cadena con el prototipo. El prototipo de user es User.prototype, y tampoco tiene constructor (¡porque “olvidamos” configurarlo correctamente!).
  3. Avanzando más en la cadena, User.prototype es un objeto simple, su prototipo es el Object.prototype incorporado.
  4. Finalmente, para el Object.prototype hay un Object.prototype.constructor == Object. Entonces es el que usa.

Como resultado, tenemos let user2 = new Object('Pete').

Probablemente no es lo que queremos. Buscábamos crear new User, no new Object. Este resultado se debe a la falta de constructor.

(Solo por si eres curioso: la llamada new Object(...) convierte su argumento a un objeto. Esto en teoría, en la práctica nadie llama new Object con un valor, y generalmente no queremos usar new Object para crear objetos en absoluto).

Mapa del Tutorial