¿Qué es el Prototype de Javascript? Un mecanismo mediante el cual los objetos en JavaScript heredan características entre sí, palabra de MDN. El problema es que Prototype, como concepto, es tan abstracto en inicio como fundamental en el corto plazo.

Y es que algo gordo esconde cuando la búsqueda de .reduce() nos lleva a resultados bajo el título Array.prototype.reduce(), mientras que .toFixed() nos lleva a Number.prototype.toFixed(), y así sucesivamente.

Tampoco podemos obviar ese __proto__, esa zona oscura de la consola presente al final de cualquier elemento que inspeccionamos: valores numéricos, arrays, objetos e incluso funciones lo tienen.

En Javascript todo parece disponer de un prototipo y todo apunta a que comprenderlo es más que conveniente. Go! 🚀

Instanciando objetos globales

La afirmación de que Javascript es un lenguaje basado en prototipos se debe a que, por poner un ejemplo, cuando crees estar declarando un array en realidad estás haciendo uso de una plantilla (el objeto global Array) que te permite sacar copias del mismo.

Estas copias se denominan en realidad instancias, pudiendo entonces afirmar que inicializar un valor supone instanciar el objeto global correspondiente.

Es por ello que estas cuatro declaraciones…


const drinks = ['Agua', 'Cacaolat', 'Jagger']
const age = 34
const hasPermission = false
const doubleValue = function(val) {
  return val * val
}

…son en realidad cuatro instancias de sus objetos globales correspondientes, pudiendo igualmente declararlas como ves a continuación:


const drinks = new Array('Agua', 'Cacaolat', 'Jagger')
const age = new Number(34)
const hasPermission = new Boolean(false)
const doubleValue = new Function('val', 'return val * val')

Cada instancia hereda el juego de métodos y propiedades provenientes de su objeto global y las almacena en su prototipo. Esta es la razón de que, tras declarar un valor, puedes hacer uso de cualquiera de sus métodos y propiedades: estás accediendo a su prototipo.

Si bien lo explicado en este artículo será aplicable a cualquier tipo de dato, ya que prácticamente todos son instancias de alguno de los objetos globales que componen el núcleo del lenguaje, tomaremos en adelante como ejemplo un array de bebidas, tal que así:


const drinks = ['Agua', 'Cacaolat', 'Jagger']

La propiedad __proto__

La consola de desarrollo es la fuente única de verdad: todo lo que el navegador concibe y por tanto podrá realizar es lo que nuestra consola mostrará. Veamos pues nuestras bebidas por consola:

Prototipo de un objeto array en Javascript

Observa cómo el lenguaje detecta que nos encontramos frente a un Array de tres posiciones 1 del que derivan dos partes diferenciadas:

  • Por un lado se encuentran los datos específicos de tu colección, identificados cada uno por su índice y que cambiarán según el contenido y longitud de cada array 2.
  • Por otro tenemos el prototipo que todas las instancias de Array compartirán y que no se ve alterado con independencia del contenido específico de cada instancia 3, representado por la propiedad __proto__ de tu objeto.

Desplegar el prototipo supone, por tanto, visualizar qué métodos y propiedades podríamos aplicar sobre cualquier array:

Prototipo de un objeto array en Javascript

Aquí conviven viejos conocidos como .forEach() con otros que quizás desconozcas y cuya función sería ideal investigar. Todos están ahí por algo.

No olvides que __proto__, aun siendo innegablemente particular, no es más que otra propiedad de este objeto. No muerde. La puedes visualizar mediante console.log(drinks.__proto__) y, naturalmente, la puedes manipular.

Pero cuidado: esta última afirmación nos lleva a nuestra siguiente parada.

Extender el prototipo

Partimos de la base más primitiva del lenguaje: todos los objetos tienen métodos y propiedades. Desde los objetos literales que declaras, hasta los resultantes de instanciar clases personalizadas u objetos globales. Apuesto que este código no te genera confusión:


const person = {
  name: 'Germán',
  age: 33,
  getNameText() {
    return `Mi nombre es ${this.name}`
  }
}

console.log(person.getNameText())
// Mi nombre es Germán

person.getAgeText = function(){
  return `Mi edad es ${this.age}`
}

console.log(person.getAgeText())
// Mi edad es 33

person.hobby = 'Beber cerveza'
console.log(`Me gusta ${person.hobby.toLowerCase()}, yay!`)
// Me gusta beber cerveza, yay!

Bien, apliquemos ahora este principio conocido a nuestras bebidas. Las instancias son a su vez objetos por lo que igualmente podemos extenderlas con nuevos métodos y propiedades:


drinks.info = 'Estas son las bebidas que me gusta tomar'
console.log(drinks.info)
// Estas son las bebidas que me gusta tomar

drinks.getFirstDrink = function(){
  return this[0]
}
console.log(`La primera bebida es ${drinks.getFirstDrink()}`)
// La primera bebida es Agua

Pero, ¿dónde se almacena esta nueva propiedad? ¿Y el método? Veamos por consola el array que hemos manipulado:

Prototipo de un objeto array en Javascript

Sin novedad: tal y como sucedería en un objeto literal, tu array dispone de estos añadidos. No los busques en su __proto__, ya que sólo ese array (y ningún otro) está dotado de estas dos propiedades adicionales.

Si lo que buscas es dotar a todos los arrays de estas configuraciones, puedes extender el prototipo de su objeto global a través de Array.prototype, o lo que es lo mismo, manipular la plantilla en la que todas tus instancias se basan:


Array.prototype.info = 'Estas son las bebidas que me gusta tomar'

Array.prototype.getFirstDrink = function(){
  return this[0]
}

const otherDrinks = ['Cerveza', 'Té']
console.log(`La primera bebida es ${otherDrinks.getFirstDrink()}`)
// La primera bebida es Cerveza

El motivo de que puedas ya hacer uso de estas inclusiones desde cualquiera de tus arrays, es que han pasado a formar parte de su prototipo:

Prototype javascript de un objeto array en Javascript

Importante mencionar que el Protoype es un objeto único y compartido por todas las instancias de un tipo determinado gracias la naturaleza hereditaria del mismo. El lenguaje no genera por tanto cada instancia con una réplica del prototipo sino que asegura que todas las instancias de una clase se enlazan al mismo prototipo. Es único, y por tanto favorable por tanto en términos de memoria.

El hecho de ser un objeto único nos da opciones tan versátiles para extenderlo como esta, obteniendo el mismo resultado:


drinks.__proto__.getFirstDrink = function(){
  return this[0]
}

const otherDrinks = ['Cerveza', 'Té']
console.log(`La primera bebida es ${otherDrinks.getFirstDrink()}`)
// La primera bebida es Cerveza

Si bien esta explicación es muy clarificadora a la hora de comprender los prototipos, extenderlos supone un antipatrón del lenguaje que por norma general se debe evitar: rompe el principio de encapsulación y modifica el estado global.

Dicho esto, no podemos evitar aprovechar las oscuras posibilidades que aquí se abren, ya que manipular el prototipo permite reconfigurar cualquiera de los métodos de su objeto global, afectando así a todas sus instancias.

A modo de prueba, hagamos que el método .push() de todos nuestros arrays elimine el primero de sus elementos en vez de incluir el elemento argumentado al final:


Array.prototype.push = function() {
  console.log(`"${elm}" no se incluirá, en su defecto suprimimos el primer item, yay!`)
  this.shift()
}

const otherDrinks = ['Cerveza', 'Té']
otherDrinks.push('Café')  
// "Café" no se incluirá, en su defecto suprimimos el primer item, yay!

console.log(`Hay ${otherDrinks.length} bebida/s presentes: ${otherDrinks}`)
// Hay 1 bebida/s presentes: Té

Conclusión

Esta ligera introducción a las bases del Prototype abre la puerta a escenarios infinitos de experimentación a través de los que asentar conceptos más avanzados como son las cadenas de prototipos, o ahondar en cuestiones interesantes como las diferencias entre las propiedades __proto__ y .prototype, que no son lo mismo.

Pero ninguna casa comienza por el tejado 😜

¿Perguntas? Nos vemos en los comentarios 🔥