⛓️

Prototype Chain

Diferentemente de outra linguagens como Java e C# (OOP), nosso querido Javascript não implementa o sistema de classes e herança no mesmo formato, mas sim através de prototypes.
Com a vinda do ES6 as classes chegaram ao Javascript, porém são apenas syntax sugar para prototypes
OOP é um paradigma de programação que envolve organizar o código em definições de objetos. Usamos objetos para modelar, encapsular e representar coisas do mundo real.
O modelo de herança clássico (classical inheritance) é limitado à apenas classes herdarem de outras classes, enquanto o modelo de herança baseado em prototypes, envolve não só prototypes herdando outros prototypes, mas também objetos herdando prototypes.
const human = { walk: () => console.log('walking') const person = {} person.__proto__ = human person.walk() // Output: walking

Herança com Prototypes

Cada objeto tem uma propriedade privada que guarda uma referência para outro objeto, que é seu prototype. E assim sucessivamente, até que o objeto tenha o prototype null. Por definição, null não tem prototype e então chegamos ao fim da prototype chain.
Quando tentamos acessar a propriedade de um objeto, ela não será procurada apenas no objeto em questão, mas também no prototype desse objeto, até que essa propriedade seja encontrada ou chegue ao fim da prototype chain.
Sabe quando usamos a propriedade length dos arrays? É a prototype chain entrando em ação.
function Phone() { this.on = function() {} } const galaxy = new Phone() galaxy.on()
No exemplo acima, a variável galaxy foi declarada usando a função construtora Phone, isso faz com que o prototype de galaxy seja Phone. Com isso, conseguimos acessar as propriedades e métodos da função Phone.
Caso o objeto galaxy também tivesse uma propriedade chamada on, então a da função Phone não seria acessada. Isso se chama property shadowing.
De acordo com a especificação ECMAScript, os objetos possuem uma propriedade interna chamada [[Prototype]], que é usada para definir o prototype desse objeto. Essa é a propriedade equivalente à __proto__, que não é padrão da especificação mas é implementada em muitos navegadores, inclusive seu uso é desencorajado.
Não devemos confundir com a propriedade prototype, que apesar de ter o mesmo nome, especifica a propriedade [[Prototype]] que será atribuída para todas as instâncias criadas a partir de um construtor. Por exemplo, Object.prototype representa o prototype Object que será aplicado a todos os objetos..

Visualizando os Prototypes

Existem dois métodos que podemos usar para interagir com os prototypes, sendo Object.getPrototypeOf e Object.setPrototypeOf, dessa forma conseguimos obter e alterar o prototype dos objetos.
const obj = {} Object.getPrototypeOf(obj) // Object const first = { a: 1 } const second = { b: 2 } Object.setPrototypeOf(second, first) second.a // 1

Um Enígma

Por que ambas as expressões retornam true?
Object instanceof Function // true Function instanceof Object // true
Primeiro precisamos entender como o operador instanceof funciona.
Trecho traduzido e retirado da MDN:
O operador instanceof testa se a função construtora aparece em qualquer lugar na prototype chain de um objeto.
objeto instanceof construtor
Agora, vejamos a especificação do operador instanceof segundo a ECMAScript:
The abstract operation InstanceofOperator(OC) implements the generic algorithm for determining if an object O inherits from the inheritance path defined by constructor C. This abstract operation performs the following steps: 1. If Type(C) is not Object, throw a TypeError exception. 2. Let instOfHandler be GetMethod(C,@@hasInstance). 3. ReturnIfAbrupt(instOfHandler). 4. If instOfHandler is not undefined, then a. Return ToBoolean(Call(instOfHandlerC, «O»)). 5. If IsCallable(C) is false, throw a TypeError exception. 6. Return OrdinaryHasInstance(CO).
Primeiro, caso o operando do lado direito não seja um objeto, uma exceção TypeError é disparada:
Function instanceof 'string' // Uncaught TypeError: Right-hand side of 'instanceof' is not an object
Em seguida, a propriedade @@hasInstance é obtida. Caso essa propriedade não seja uma função, uma nova exceção TypeError é disparada:
Function instanceof {} // Uncaught TypeError: Right-hand side of 'instanceof' is not callable
Conseguimos simular isso usando Symbol.hasInstance:
const obj = { [Symbol.hasInstance]: 1, } Function instanceof obj // Uncaught TypeError: number 1 is not a function
O resultado da execução do método @@hasInstance é convertido para booleano, e então retornado.
Os passos 5 e 6 servem para prover retrocompatibilidade com especificações anteriores à ES6 que não usavam essa propriedade interna @@hasInstance. Caso alguma função não defina ou herde essa propriedade, então a semântica padrão do instanceof é usada:
The abstract operation OrdinaryHasInstance implements the default algorithm for determining if an object O inherits from the instance object inheritance path provided by constructor C. This abstract operation performs the following steps: 1. If IsCallable(C) is false, return false. a. If C has a [[BoundTargetFunction]] internal slot, then b. Let BC be the value of C’s [[BoundTargetFunction]] internal slot. 2. Return InstanceofOperator(OBC) (see 12.9.4). 3. If Type(O) is not Object, return false. 4. Let P be Get(C"prototype"). 5. ReturnIfAbrupt(P). 6. If Type(P) is not Object, throw a TypeError exception. 7. Repeat a. Let O be O.[[GetPrototypeOf]](). b. ReturnIfAbrupt(O). c. If O is null, return false. d. If SameValue(PO) is true, return true.
De forma resumida e simples, o que acontece aqui é: a engine obtém a propriedade prototype do construtor (operando do lado direito) e compara com a propriedade interna [[Prototype]] do objeto (operando do lado esquerdo) de forma recursiva, até que o prototype do objeto seja null (nesse caso vai retornar false) ou a propriedade prototype do construtor seja a mesma do objeto.
Agora vamos voltar ao nosso exemplo:
Object instanceof Function // true
A engine vai checar se a propriedade Function.prototype existe na prototype chain do Object:
Function.prototype // ƒ () { [native code] } Object.getPrototypeOf(Object) // ƒ () { [native code] } Object.getPrototypeOf(Object) === Function.prototype // true
Nesse caso a propriedade Function.prototype é a mesma da propriedade interna [[Prototype]] do Object, isso se dá pois Object é de fato uma função construtora (typeof Object === 'function'), logo o retorno é true.
Agora considerando o segundo caso:
Function instanceof Object // true Object.prototype // {} Object.getPrototypeOf(Function) // ƒ () { [native code] } Object.getPrototypeOf(Function) === Object.prototype // false Object.getPrototypeOf(Object.getPrototypeOf(Function)) // {} - Object.prototype Object.getPrototypeOf(Object.getPrototypeOf(Function)) === Object.prototype // true
  1. Obtém a propriedade prototype do construtor Object
  1. Obtém o prototype do objeto Function
  1. Compara o prototype do objeto com a propriedade prototype do construtor
    1. O resultado é false pois Object.prototype é um objeto de onde derivam todos os outros objetos, enquanto que o prototype de Function é Function.prototype, uma função.
  1. Aqui entram os passos de repetição, onde o prototype do prototype do objeto é obtido de forma recursiva até que seja null, ou igual a propriedade prototype do construtor:
    1. O prototype de Function é Function.prototype,
    2. O prototype de Function.prototype é Object.prototype, pois, novamente, funções também são objetos.
    3. Logo, o resultado dessa comparação é true, pois Object.prototype (do construtor) é igual a Object.prototype (o prototype do prototype, do objeto Function)

Referências