Prototipos de objetos

Estás leyendo la versión en inglés del artículo porque aún no existe una traducción para este idioma. ¡Ayúdanos a traducir este artículo!

Los prototipos son un mecanismo mediante el cual los objetos en JavaScript heredan características entre sí. En este artículo, explicaremos como funcionan los prototipos y también cómo se pueden usar las propiedades de los prototipos para añadir métodos a los contructores existentes.

Prerequisites: Conocer las funciones en Javascript, conocimientos básicos de Javascript (ver Primeros Pasos y Building blocks) y Javascript orientado a Objetos (ver Introducción a Objetos).
Objective: Comprender los prototipos de objectos de Javascript, cómo la cadena de prototype funciona, y cómo añadir nuevos métodos a la propiedad prototype.

¿Un lenguaje basado en prototipos?

JavaScript es a menudo descrito como un lenguaje basado en prototipos. Para proporcionar mecanismos de herencia los objetos pueden tener un prototipo (objeto prototipo) asociado, que actúa como una plantilla desde la que el objeto puede heredar métodos y propiedades. Un objeto prototipo puede tener, a su vez, otro objeto prototipo asociado desde el que heredar métodos y propiedades. Esto es conocido como la cadena de prototipos, y es la razón por la que los objetos pueden tener métodos y propiedades disponibles que no han sido declarados por ellos mismos.

Aunque para ser exactos, los métodos y propiedades son definidas en la propiedad prototype, que reside en la función constructor del objeto, no en la instancia del objeto.

Así pues tenemos que, en JavaScript, se establece un enlace entre la instancia del objeto y su prototipo (este se encuentra en la propiedad __proto__ de la instancia, que es inicializada por la propiedad prototype del constructor). Y, como ya se ha comentado, el objeto tendrá acceso a una serie de métodos y propiedades que se encuentran a lo largo de la cadena de prototipos asociada.

Nota: Es importante entender que, tanto el prototipo de la instancia de un objeto (al cual se accede mediante Object.getPrototypeOf(obj), o a través de la propiedad __proto__) como el prototipo que contiene el constructor (que se encuentra en la propiedad prototype del constructor) hacen referencia al mismo objeto.

Vamos a echar un vistazo a algunos ejemplos para intentar aclarar estos conceptos.

Entendiendo objectos prototipos

Volvamos al ejemplo anterior en el que acabamos definiendo nuestro constructor Person() — cargue el ejemplo en su navegador. Si aún no lo tienes luego de haber trabajado el último artículo, usa nuestro ejemplo oojs-class-further-exercises.html (vea también el código fuente).

En este ejemplo, hemos definido una función constructor, así:

function Persona(nombre, apellido, edad, genero, intereses) {
  
  // definiendo de propiedades y métodos
  this.first = first;
  this.last = last;
//...
}

We have then created an object instance like this:

var person1 = new Persona('Bob', 'Smith', 32, 'hombre', ['music', 'skiing']);

Si tipea "person1." en su consola JavaScript, debería ver que el navegador intenta completarlo automáticamente con los nombres de miembro disponibles en este objeto:

En esta lista, podra ver los miembros definidos en el objeto prototipo de person1, que es la Persona() (Persona() es el constructor) - nombre, edad, género, intereses, biografía y saludos. Sin embargo, también verá algunos otros miembros - watch, valueOf, etc - que están definidos en el objeto prototipo de Persona() 's, que es un Objeto (Object). Esto demuestra que el prototipo cadena funciona.

Entonces, ¿qué sucede si llama a un método en person1, que está definido en Object? Por ejemplo:

person1.valueOf()

Este método simplemente retornará el valor del objeto sobre el que se llama - ¡pruébalo y verás! En este caso, lo que pasa es que:

  • El navegador comprueba inicialmente si el objeto person1 tiene un método valueOf() disponible en él.
  • Si No lo hace, entonces el navegador comprueba si el objeto prototipo del objeto person1 (el prototipo del constructor de Person()) tiene un método valueOf() disponible en él.
  • Si tampoco lo hace, entonces el navegador comprueba si el objeto prototipo del objeto prototipo del constructor Persona() (Objeto() prototipo del objeto prototipo del constructor) tiene un método valueOf() disponible en él. Lo hace, así que es llamado, y todo funciona!

ota: Queremos reiterar que los métodos y propiedades no se copian de un objeto a otro en la cadena del prototipo, sino que se accede a ellos subiendo por la cadena como se ha descrito anteriormente.

Nota: No existe oficialmente una forma de acceder directamente al objeto prototipo de un objeto - los "enlaces" entre los elementos de la cadena están definidos en una propiedad interna, denominada [[prototipo]] en la especificación del lenguaje JavaScript (ver ECMAScript). La mayoría de los navegadores modernos, sin embargo, tienen una propiedad disponible llamada __proto__ (es decir, 2 subrayados en cada lado), que contiene el objeto prototipo del constructor del objeto. Por ejemplo, pruebe person1.__proto__ y person1.__proto__.__proto__ para ver cómo se ve la cadena en código!

Desde ECMAScript 2015 se puede acceder indirectamente al objeto prototipo de un objeto mediante Object.getPrototypeOf(obj).

Propiedades del prototipo: Donde se definen los miembros hereditarios

Entonces, ¿dónde se definen las propiedades y métodos heredados? Si miras la página de referencia del objeto, verás en la parte izquierda un gran número de propiedades y métodos - muchos más que el número de miembros heredados que vimos disponibles en el objeto person1. Algunos son hereditarios y otros no, ¿por qué?

La respuesta es que los heredados son los que están definidos en la propiedad prototipo (podría llamarse subespacio de nombres), es decir, los que empiezan con Object.prototype, y no los que empiezan sólo con Object. El valor de la propiedad del prototipo es un objeto, que es básicamente un repositorio(bucket) para almacenar propiedades y métodos que queremos que sean heredados por los objetos más abajo en la cadena del prototipo.

Así que Object.prototype.watch(), Object.prototype.valueOf(), etc., están disponibles para cualquier tipo de objeto que herede de Object.prototype, incluyendo nuevas instancias de objeto creadas desde el constructor.

Object.is(), Object.keys(), y otros miembros no definidos dentro del prototipo del repositorio(bucket) no son heredados por instancias de objeto o tipos de objeto que heredan de Object.prototype. Sino que son métodos/propiedades disponibles sólo en el propio constructor Object().

Nota: Esto parece extraño - ¿cómo se puede tener un método definido en un constructor, que en sí mismo es una función? Bueno, una función es también un tipo de objeto - vea la referencia del constructor de Function() si no nos cree.

  1. Puede comprobar las propiedades de los prototipos existentes - vuelva a nuestro ejemplo anterior e intente introducir lo siguiente en la consola JavaScript:
    Person.prototype
  2. El resultado no le mostrará mucho - después de todo, no hemos definido nada en el prototipo de nuestro constructor personalizado! Por defecto, el prototipo de un constructor siempre comienza vacío. Ahora intente lo siguiente:
    Object.prototype

Verá un gran número de métodos definidos en la propiedad Prototype de Object, que están disponibles en los objetos que heredan de Object, como se ha mostrado anteriormente.

Verá otros ejemplos de herencia de cadena de prototipos en todo JavaScript - intente buscar los métodos y propiedades definidas en el prototipo de los objetos globales String, Date, Number y Array, por ejemplo. Todos ellos tienen un número de miembros definidos en su prototipo, por lo que, por ejemplo, cuando se crea una cadena, como ésta:

var myString = 'Esto es mi String.';

myString inmediatamente tiene una serie de métodos útiles disponibles en él, como split(), indexOf(), replace(), etc.

Importante: La propiedad prototipo es una de las partes más confusamente nombradas de JavaScript - podría pensarse que apunta al objeto prototipo del objeto actual, pero no lo hace (es un objeto interno al que puede accederse mediante __proto__, ¿recuerda?). en su lugar, el prototipo es una propiedad que contiene un objeto en el que se definen los miembros en el que se desea que se hereden.

Revisando create()

Anteriormente mostramos cómo Object.create() crea una nueva instancia de objeto.

  1. Por ejemplo, pruebe esto en la consola JavaScript de su ejemplo anterior:
    var person2 = Object.create(person1);
  2. Lo que hace create() es crear un nuevo objeto a partir de un objeto prototipo específico. Aquí, la person2 se crea utilizando la person1 como objeto prototipo. Puede comprobarlo introduciendo lo siguiente en la consola:
    person2.__proto__

Esto devolverá el objeto Persona.

Propiedades del constructor

Cada función de constructor tiene una propiedad de prototipo cuyo valor es un objeto que contiene una propiedad de constructor. Esta propiedad del constructor apunta a la función original del constructor. Como verá en la siguiente sección que las propiedades definidas en la propiedad Person.prototype (o en general en la propiedad prototipo de una función de constructor, que es un objeto, como se mencionó en la sección anterior) están disponibles para todos los objetos de instancia creados utilizando el constructor Person(). Por lo tanto, la propiedad del constructor también está disponible tanto para los objetos persona1 como para los objetos persona2.

  1. Por ejemplo, pruebe estos comandos en la consola:
    person1.constructor
    person2.constructor

    These should both return the Person() constructor, as it contains the original definition of these instances.

    A clever trick is that you can put parentheses onto the end of the constructor property (containing any required parameters) to create another object instance from that constructor. The constructor is a function after all, so can be invoked using parentheses; you just need to include the new keyword to specify that you want to use the function as a constructor.

  2. Try this in the console:
    var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
  3. Now try accessing your new object's features, for example:
    person3.name.first
    person3.age
    person3.bio()

This works well. You won't need to use it often, but it can be really useful when you want to create a new instance and don't have a reference to the original constructor easily available for some reason.

The constructor property has other uses. For example, if you have an object instance and you want to return the name of the constructor it is an instance of, you can use the following:

instanceName.constructor.name

Try this, for example:

person1.constructor.name

Note: The value of constructor.name can change (due to prototypical inheritance, binding, preprocessors, transpilers, etc.), so for more complex examples you'll want to use the instanceof operator instead. 

Modifying prototypes

Let's have a look at an example of modifying the prototype property of a constructor function (methods added to the prototype are then available on all object instances created from the constructor).

  1. Go back to our oojs-class-further-exercises.html example and make a local copy of the source code. Below the existing JavaScript, add the following code, which adds a new method to the constructor's prototype property:
    Person.prototype.farewell = function() {
      alert(this.name.first + ' has left the building. Bye for now!');
    };
  2. Save the code and load the page in the browser, and try entering the following into the text input:
    person1.farewell();

You should get an alert message displayed, featuring the person's name as defined inside the constructor. This is really useful, but what is even more useful is that the whole inheritance chain has updated dynamically, automatically making this new method available on all object instances derived from the constructor.

Think about this for a moment. In our code we define the constructor, then we create an instance object from the constructor, then we add a new method to the constructor's prototype:

function Person(first, last, age, gender, interests) {

  // property and method definitions

}

var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);

Person.prototype.farewell = function() {
  alert(this.name.first + ' has left the building. Bye for now!');
};

But the farewell() method is still available on the person1 object instance — its available functionality has been automatically updated.

Note: If you are having trouble getting this example to work, have a look at our oojs-class-prototype.html example (see it running live also).

You will rarely see properties defined on the prototype property, because they are not very flexible when defined like this. For example you could add a property like so:

Person.prototype.fullName = 'Bob Smith';

This isn't very flexible, as the person might not be called that. It'd be much better to do this, to build the fullName out of name.first and name.last:

Person.prototype.fullName = this.name.first + ' ' + this.name.last;

This however doesn't work, as this will be referencing the global scope in this case, not the function scope. Calling this property would return undefined undefined. This worked fine on the method we defined earlier in the prototype because it is sitting inside a function scope, which will be transferred successfully to the object instance scope. So you might define constant properties on the prototype (i.e. ones that never need to change), but generally it works better to define properties inside the constructor.

In fact, a fairly common pattern for more object definitions is to define the properties inside the constructor, and the methods on the prototype. This makes the code easier to read, as the constructor only contains the property definitions, and the methods are split off into separate blocks. For example:

// Constructor with property definitions

function Test(a, b, c, d) {
  // property definitions
}

// First method definition

Test.prototype.x = function() { ... };

// Second method definition

Test.prototype.y = function() { ... };

// etc.

This pattern can be seen in action in Piotr Zalewa's school plan app example.

Summary

This article has covered JavaScript object prototypes, including how prototype object chains allow objects to inherit features from one another, the prototype property and how it can be used to add methods to constructors, and other related topics.

In the next article we'll look at how you can implement inheritance of functionality between two of your own custom objects.

In this module