DEV Community

Cover image for Prototypes en JavaScript
Diego Maximiliano
Diego Maximiliano

Posted on • Updated on

Prototypes en JavaScript

Parte del problema a la hora de entender que son y como funcionan los prototypes en Js es no pensar en las preguntas correctas, en vez de preguntarse:

  • Que son los prototypes?
  • Como funcionan?

Un mejor enfoque sería preguntar:

  • Por que existen?
  • Que problema resuelven?

Si logramos responder esas preguntas podemos tener una base más solida y una comprensión más consolidada, esta técnica puede aplicarse a la hora de aprender otros conceptos.
Esta idea la aprendí de este video sobre Hooks en React de Tyler Mcginnis.

Empecemos con el problema ☠️:

Supongamos que escribimos una clase en PHP la cual tiene como fin darnos una abstracción a la base de datos, no estoy aplicando DataMapper o Active Record, como hacen la mayoría de los frameworks, es solo para tener una idea:

<?php

class DbAccess
{
  protected $tableName;

  function __construct($tableName)
  {
    $this->tableName = $tableName;
  }

  public function getAll()
  {
    // select all records from table
  }

  public function getRecord($id)
  {
    // select record by id
  }

  public function deleteRecord($id)
  {
    // delete record by id
  }

}
Enter fullscreen mode Exit fullscreen mode
  • Tenemos una clase con 1 atributo $tableName y 3 métodos:
  • getAll()
  • getRecord()
  • deleteRecord()

Podemos visualizar la clase con la siguiente tabla:

Atributos Métodos
$tableName getAll()
getRecord()
deleteRecord()

Nada complicado por ahora ⏳😃.

Ahora la implementamos:

<?php

class EmployeeController 
{
    public function delete($id)
    {
      // nuevo objeto en memoria
      $dbManager = new DbAccess('Employee'); 
      // solo estoy invocando un método de 3 🤔
      $dbManager->deleteRecord($id);
      return redirect('home');
    }
}
Enter fullscreen mode Exit fullscreen mode

1) Creamos el nuevo objecto/instancia (nuevo objecto en memoria).
2) El objeto tiene 1 atributo y 3 métodos (todos almacenados en memoria)
3) Sólo estoy usando un método ($dbManager->deleteRecord()) ⚠️ 🤔.

A medida que la aplicación crece seguramente vamos a necesitar agregar más métodos y atributos así tenemos más operaciones disponibles, vamos a suponer:

  • Crear un nuevo registro.
  • Modificar un registro existente.
  • Hacer una transacción.
  • Obtener el primer registro.
  • Obtener el último registro.

De repente esta es nuestra clase:

Atributos Métodos
$tableName getAll()
getRecord()
deleteRecord()
createRecord()
transaction()
getFirst()
update()
getLast()

Volviendo al ejemplo anterior:

<?php

class EmployeeController 
{
    public function delete($id)
    {
      // nuevo objeto en memoria (incluyendo métodos y atributos)
      $dbManager = new DbAccess('Employee'); 
      // solo estoy invocando un método de 8 😨
      $dbManager->deleteRecord($id);
      return redirect('home');
    }
}
Enter fullscreen mode Exit fullscreen mode

Agreguemos un caso un poco más complejo 🔥:

<?php

class EmployeeController 
{
    public function delete($id)
    {

      $employeeDbManager = new DbAccess('Employee'); 
      // Obtengo people_id en la tabla Employee
      $peopleId = $employeeDbManager->getRecord($id, 'people_id');

      // Ahora creo otra instancia más
      $personDbManager = new DbAccess('Person');
      // Borro los registros de ambas tablas
      $personDbManager->deleteRecord($peopleId)
      $employeeDbManager->deleteRecord($id);
      // Ahora tengo dos objetos, 16 métodos y solo uso 3 
      // 😱
      return redirect('home');
    }
}
Enter fullscreen mode Exit fullscreen mode

Imaginemos ahora que usemos 5 intancias y usemos un método de cada una, de repente tenemos 5 objetos, 40 métodos, cargados en memoria y solo usamos 5, este es el problema ☠️. La clase sirve como blueprint y nos devuelve objetos (se guardan en memoria) de los cuales muchas veces solo necesitamos un atributo o método específico.
Y este problema tiene nombre: El gorila con la banana 🐒🍌. Sí, el nombre es gracioso, hace referencia al hecho de que cuando "solicitamos" una banana, lo que obtenemos en realidad es un gorila sosteniendo una 🍌. Este problema es propio de los lenguajes de programación Orientados a Objectos basados en Clases (PHP, Java, C++), una parte del problema se resuelve usando métodos staticos, pero aún existe otro problema, que los métodos estáticos no tienen acceso al contexto del objeto (no se puede usar la keyword $this en la implementación).

JavaScript por otro lado, resuelve ambos problemas usando un mecanismo llamado prototypes (prototipos).

Funciones constructoras en JavaScript

Toda variable en JavaScript es un objeto, contiene ciertos métodos que los "hereda" de la clase principal Object, pero a diferencia de otros lenguajes este sistema de herencia funciona de una mejor manera gracias a los prototypes, pero primero veamos como creamos nuestros propios objectos con JavaScript o simulamos clases (en ES6 existen las clases pero son una abstracción de la implementación interna basada en prototypes).
Generalmente las funciones constructoras tienen el nombre en mayúscula y se ejecutan usando el operador new():

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person = new Person('Rick', 50);
// esto nos devuelve un objeto plano tal como:
const rick = {
  name: 'Rick',
  age: 50
};
Enter fullscreen mode Exit fullscreen mode

Que pasa si llamo a la función Person sin el operador new? 🤔

const person = Person('Diego', 29); // undefined
Enter fullscreen mode Exit fullscreen mode

What? 🤯

Hay algo importante que tenemos que entender sobre el operador new, lo que hace es ejecutar la función con unos pasos adicionales, para retornar el objeto que necesitamos, el proceso es un poco más complejo, pero para hacerlo simple:

  • Crea un nuevo objeto plano.
  • Asigna el valor de this (contexto) a ese objeto.
  • Retorna el objeto al final.

Podemos visualizarlo de esta manera:

function Person(name, age) {
  const obj = {}; // Nuevo objeto
  this = obj; // Asigna el contexto
  this.name = name;
  this.age = age;

  return this; // {name: 'Diego, age: 29}
}
Enter fullscreen mode Exit fullscreen mode

Entonces, si ejecutamos la función sin el operador new, JavaScript la ejecuta como una función normal, y al no retornar nada explicítamente, por defecto devuelve undefined.

Function.prototype y Object.getPrototypeOf()

Al fin 😅.

Todas las funciones en JavaScript excepto las Arrow Functions tienen una propiedad llamada prototype, y se usa para guardar en ella lós métodos y atributos que queremos que los objectos hereden:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 👇👇👇👇👇👇👇👇👇
Person.prototype.sayName = function() {
  console.log(this.name);
}

const person = new Person('Max', 25);
person.sayName(); // Max

// ✋🛑 ‼️
// el método sayName() no parece estar en el objeto:
console.log(person) // {name: 'Max', age: 25}
// 😱
Enter fullscreen mode Exit fullscreen mode

#2 🤯 What?

Los objectos también tienen una propiedad especial [[Prototype]]:

La forma de acceder a la propiedad [[Prototype]] de un objeto es usando el método Object.getPrototypeOf()

// 👇👇👇👇👇👇👇👇👇👇
Object.getPrototypeOf(person) // Person {sayName: [λ]}
Enter fullscreen mode Exit fullscreen mode

☝️ Dentro de la propiedad [[Prototype]] encontramos un objeto con el método sayName 😮

Nota: antes se podía acceder al prototipo de un objecto usando la propiedad especial .__proto__ pero ahora es obsoleta:

// .__proto__ es obsoleta ❌
console.log(person.__proto__) // Person {sayName: [λ]}
Enter fullscreen mode Exit fullscreen mode

Esto es lo que en JavaScript se conoce como prototype chaining, el engine primero busca el método en el objeto, y si no lo encuentra busca dentro de la propiedad especial [[Prototype]] la cual guarda una referencia al método, que está en en el prototype de la función constructora, por eso también lo llaman delegación, entonces los objetos solo guardan referencias con su contexto seteado, volvamos al ejemplo para entender a que nos referimos con contexto (o el valor de this), el cual es muy importante:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayName = function() {
  // 👇👇👇👇👇👇👇👇👇👇
  console.log(this.name);
}

const person = new Person('Max', 25);
person.sayName(); // Max
Enter fullscreen mode Exit fullscreen mode

Al llamar al método person.sayName() el engine setea el valor de this y le asigna el valor del objeto que lo está llamando, en este caso person que tiene una propiedad name.

Múltiples instancias 👩‍👧‍👦:


function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayName = function() {
  console.log(this.name);
}

const max = new Person('Max', 25).sayName(); // Max
console.log(max) // {name: 'Max', age: 25}

const rick = new Person('Rick', 55).sayName() // Rick
console.log(rick) // {name: 'Rick', age: 55}

const morty = new Person('Morty', 15).sayName() // Morty
console.log(morty) // {name: 'Morty', age: 15}
Enter fullscreen mode Exit fullscreen mode

En este caso, los 3 objetos, max, ricky y morty sólo tienen 2 propiedades, y dentro de su propiedad especial [[Prototype]] contienen la referencia al método Person.prototype.sayName, así los objectos son más livianos.

Objetos como prototypes:

Con el método Object.setPrototypeOf() podemos definir la propiedad [[Prototype]] o el prototipo de un objeto:


const readOnlyPermissions = {
  read: true,
  write: false
}

const manager = {
  name: "Paul",
  age: 40,
}

Object.setPrototypeOf(manager, readOnlyPermissions);

console.log(manager.read); // true
Enter fullscreen mode Exit fullscreen mode

Este caso es mucho más inusual de ver, pero básicamente podemos hacer que objetos hereden de otros objectos, por eso se considera a JavaScript "verdaderamente" Orientado a Objetos.

Clases en ES2015

ES2015 o más conocido como ES6 introdujo el concepto de clases en JavaScript, que es una abstracción o syntactic sugar para definir una función constructora junto con su prototype:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayName() {
    console.log(this.name);
  }
}

const person = new Person('Rick', 55);
console.log(person) // {name: 'Rick', age: 55}
person.sayName(); // Rick
Object.getPrototypeOf(person) // Person {sayName: [λ]}
Enter fullscreen mode Exit fullscreen mode

Ahora que entendemos los prototypes, como funcionan y que problema resuelven podemos entender mejor JavaScript como lenguaje, y como los objectos nativos manejan su herencia, es por eso que métodos en Arrays como Array.prototype.map están documentados con esa sintáxis, la clase Array tiene métodos como .map(), .reduce() y .filter() dentro de su prototype.

Referencias:

[video] Prototypes in JavaScript - FunFunFunction #16

Gracias por leer!

Diego.

Latest comments (1)

Collapse
 
alexistec profile image
Alexis

Excellent article, great