Prototypes en JavaScript
Tags: javascript, oop
Published: 2020-06-16 22:02:58

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](https://www.youtube.com/watch?v=eX_L39UvZes) sobre Hooks en React de [Tyler Mcginnis](https://twitter.com/tylermcginnis).

## 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
<?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
  }

}
```
* 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
<?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');
    }
}
```
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
<?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');
    }
}
```
### Agreguemos un caso un poco más complejo 🔥:

```php
<?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');
    }
}
```

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 🐒🍌](https://www.johndcook.com/blog/2011/07/19/you-wanted-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](https://www.php.net/manual/en/language.oop5.static.php#language.oop5.static.methods), 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new):

```js
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
};
```
### Que pasa si llamo a la función `Person` sin el operador `new`? 🤔
```js
const person = Person('Diego', 29); // undefined
```
## <center>What? 🤯</center>

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:

```js
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}
}
```
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) tienen una propiedad llamada **[prototype](https://javascript.info/function-prototype)**, y se usa para guardar en ella lós métodos y atributos que queremos que los objectos hereden:

```js
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}
// 😱
```
## <center>#2 🤯 What?</center>

### 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()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf)
```js
// 👇👇👇👇👇👇👇👇👇👇
Object.getPrototypeOf(person) // Person {sayName: [λ]}
```
☝️ 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto):
```js
// .__proto__ es obsoleta ❌
console.log(person.__proto__) // Person {sayName: [λ]}
```

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:

```js
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
```
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 👩‍👧‍👦:

```js

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}
```

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()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) podemos definir la propiedad `[[Prototype]]` o el `prototipo` de un objeto:

```js 

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

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

Object.setPrototypeOf(manager, readOnlyPermissions);

console.log(manager.read); // true
```
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class) en JavaScript, que es una abstracción o *syntactic sugar* para definir una función constructora junto con su `prototype`: 
```js
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: [λ]}
```
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) están documentados con esa sintáxis, la clase `Array` tiene métodos como `.map()`, [.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) y [.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Filter) dentro de su **prototype**.


## Referencias:
[ [video] Prototypes in JavaScript - FunFunFunction #16](https://www.youtube.com/watch?v=riDVvXZ_Kb4)


Gracias por leer!

#### [Diego.](https://twitter.com/muydiego)