É 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.
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 { … }
*/
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
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;
}
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 ()
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 { … }
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 { … }
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 ()
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 ()
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) }
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) }
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 ()
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)