DEV Community

EronAlves1996
EronAlves1996

Posted on

Como o prototype chain do Javascript é uma Linked List

É galera, entender o prototype chain do Javascript é um pouquinho mais difícil do que imaginava porque requer um entendimento básico sobre linked lists.

Se você não entende, vou te explicar o básico.

Linked List

Uma Linked List, ou lista ligada, é uma lista de elementos onde cada elemento aponta para o próximo elemento da lista. Se formos pensar como objetos, teríamos o conteúdo do objeto e uma variável especial apontando para o próximo elemento da lista.

Linked List

Cada vez que você adicionar um elemento nessa lista, você tem que assegurar que o último elemento da lista vai apontar para este próximo elemento, elegendo este recém-adicionado elemento como o último elemento. Acho que é válido tentar ao menos criar uma implementação básica em qualquer linguagem para entender como ela funciona.

Sobre Prototypes

Entendendo sobre linked lists, conseguimos entender de forma mais fácil a prototype chain. Acontece que toda função e objeto criados em Javascript, possuem uma propriedade .prototype associada, seja ela uma propriedade explicita ou uma propriedade interna.

let someString = {abc:"abc"}
console.log(someString)
/*
> Object { abc: "abc" }
abc: "abc"
<prototype>: Object { … }
*/
Enter fullscreen mode Exit fullscreen mode

Se formos investigar este prototype, ele possui uma propriedade proto, que possui mais uma propriedade proto que aponta para null. Essas propriedades proto são as propriedades .prototype internas do objeto. Cada objeto terá seu prototype onde eles vão apontar para uma outra prototype até o último prototype apontar para null, encerrando a cadeia de prototypes:

Object { abc: "abc" }
abc: "abc"
<prototype>: Object {  }​​
  __defineGetter__: function __defineGetter__()
  __defineSetter__: function __defineSetter__()
  ​​__lookupGetter__: function __lookupGetter__()
​​  __lookupSetter__: function __lookupSetter__()
​​  > __proto__: Object {  }
​​​      __defineGetter__: function __defineGetter__()​​
      __defineSetter__: function __defineSetter__()
​​​      __lookupGetter__: function __lookupGetter__()
​​​      __lookupSetter__: function __lookupSetter__()
​​​      __proto__: null
Enter fullscreen mode Exit fullscreen mode

Só isso já é evidência suficiente de que a prototype chain é uma linked list e que, para trabalhar com objetos, é necessário um conhecimento básico de referências.

Em C, por exemplo, linked lists são implementadas utilizando ponteiros, e a lógica utilizada por baixo dos panos na prototype chain é a mesma aqui: um ponteiro na propriedade .prototype (ou proto) apontando para outros prototypes até chegar em um null.

Por quê saber disso?

O estilo de objetos e herança utilizados em Javascript é baseado em prototypes. Então pense que o objeto não será apenas ele e seu construtor, mas uma cadeia de prototypes que dá uma certa capacidade para realizar vários chainings. Pense que se um método ou propriedade não está disponível imediatamente no objeto onde ele está sendo chamado, o runtime irá seguir toda a cadeia de prototypes até achar uma combinação para execução ou retorno de um valor. Isso é mais útil quando utilizamos Object.create e ocultamos as propriedades retornadas pelo Prototype por outras.

Redefinindo prototypes

Por último, vou utilizar um pequeno exemplo do MDN onde, através de um construtor, é redefinido o prototype de uma função:

const personPrototype = {
  greet() {
    console.log(`hello, my name is ${this.name}!`);
  }
}

function Person(name) {
  this.name = name;
}
Enter fullscreen mode Exit fullscreen mode

Definimos aqui um object literal e uma função. Vamos investigar seus prototypes através do console.log:

>> console.log(personPrototype)
  > Object { greet: greet() }
      greet: function greet()
      <prototype>: Object {  }

>> console.log(Person)
    > function Person(name)
        arguments: null
        caller: null
        length: 1
        name: "Person"
        prototype: Object {  }
        <prototype>: function ()
Enter fullscreen mode Exit fullscreen mode

Preste muita atenção. A função Person tem um prototype ativo (não é apenas um prototype interno) que já foi reapontado pelo próprio runtime. Se abrir ela, vai ver que este prototype aponta para ela mesma.

> prototype: Object {  }
​​    constructor: function Person(name)
    <prototype>: Object {  }
Enter fullscreen mode Exit fullscreen mode

Isso é importante, pois quando for criado um novo objeto passando o Person como função construtora, a propriedade .prototype vai ser "copiada" para este novo objeto, contendo o construtor, ou seja, todo objeto criado a partir deste vai ter sua .prototype apontando para esta função.

let eron = new Person("eron");
undefined

console.log(eron)
> Object { name: "eron" }
    name: "eron"
    > <prototype>: Object {  }
        constructor: function Person(name)
​​        <prototype>: Object {  }
Enter fullscreen mode Exit fullscreen mode

Agora o objeto não tem este mesmo prototype, utilizando o que a própria linguagem declara para ele.

Feito isto, em Person, vamos então reapontar seu .prototype para o objeto PersonPrototype:

Person.prototype = personPrototype;
Object { greet: greet() }

console.log(Person)
> function Person(name)
    arguments: null
    caller: null
    length: 1
    name: "Person"
  > prototype: Object { greet: greet() }​​
      greet: function greet()​​
      <prototype>: Object {  }
    <prototype>: function ()
Enter fullscreen mode Exit fullscreen mode

Percebam que o construtor sumiu do .prototype. Por quê? Porque o prototype agora está apontando para a função personPrototype, então temos que consertar isso. Se não, se for criado um novo objeto dessa forma e outro programador quiser seu constructor, ele nunca vai saber, nem mesmo se ele mergulhar no prototype chain:

let carina = new Person("Carina"); 

carina.constructor
> function Object()
  assign: function assign()
  create: function create()
  defineProperties: function defineProperties()
  defineProperty: function defineProperty()
  entries: function entries()
  freeze: function freeze()
  fromEntries: function fromEntries()
  getOwnPropertyDescriptor: function getOwnPropertyDescriptor()
  getOwnPropertyDescriptors: function getOwnPropertyDescriptors()
  getOwnPropertyNames: function getOwnPropertyNames()
  getOwnPropertySymbols: function getOwnPropertySymbols()
  getPrototypeOf: function getPrototypeOf()
  hasOwn: function hasOwn()
  is: function is()
  isExtensible: function isExtensible()
  isFrozen: function isFrozen()
  isSealed: function isSealed()
  keys: function keys()
  length: 1
  name: "Object"
  preventExtensions: function preventExtensions()
  prototype: Object {  }
  seal: function seal()
  setPrototypeOf: function setPrototypeOf()
  values: function values()
  <prototype>: function ()
Enter fullscreen mode Exit fullscreen mode

Vamos consertar isso atrelando um construtor à personPrototype. Nesse caso, ou atrelamos esse construtor direto no objeto personProtoype, ou descemos o prototype chain usando os ponteiros.

Opção 1:

personPrototype.constructor = Person; 

console.log(Person)
> function Person(name)
    arguments: null
    caller: null
    length: 1
    name: "Person"
    prototype: Object { greet: greet(), constructor: Person(name) }
Enter fullscreen mode Exit fullscreen mode

Opção 2:

Person.prototype.constructor = Person; 

console.log(Person)
> function Person(name)
    arguments: null
    caller: null
    length: 1
    name: "Person"
    prototype: Object { greet: greet(), constructor: Person(name) }
Enter fullscreen mode Exit fullscreen mode

Feito isso, como o constructor é um ponteiro, chamando-o novamente em cima do objeto criado anteriormente, verificamos que o comportamento agora está ok:

carina.constructor
> function Person(name)
  arguments: null
  caller: null
  length: 1
  name: "Person"
  prototype: Object { greet: greet(), constructor: Person(name) }
  <prototype>: function ()
Enter fullscreen mode Exit fullscreen mode

Basicamente é isso. Prototype chain é difícil, mas depois que você entende, vê o conceito básico que está por trás dele, acredito que fique mais fácil de entender.

Para saber mais

Seção #Objetos do ECMAScript
Herança e a cadeia de Protótipos

Top comments (0)