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(O, C) 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(instOfHandler, C, «O»)). 5. If IsCallable(C) is false, throw a TypeError exception. 6. Return OrdinaryHasInstance(C, O).
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 isnull
, return false. d. If SameValue(P, O) 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
- Obtém a propriedade
prototype
do construtorObject
- Obtém o prototype do objeto
Function
- Compara o prototype do objeto com a propriedade
prototype
do construtor - O resultado é
false
poisObject.prototype
é um objeto de onde derivam todos os outros objetos, enquanto que o prototype deFunction
éFunction.prototype
, uma função.
- 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 propriedadeprototype
do construtor: - O prototype de
Function
éFunction.prototype
, - O prototype de
Function.prototype
éObject.prototype
, pois, novamente, funções também são objetos. - Logo, o resultado dessa comparação é
true
, poisObject.prototype
(do construtor) é igual aObject.prototype
(o prototype do prototype, do objetoFunction
)