A propriedade "prototype"
é amplamente utilizada pelo próprio núcleo do JavaScript. Todas as funções construtoras integradas o utilizam.
Primeiro veremos os detalhes e depois como usá-lo para adicionar novos recursos a objetos integrados.
Digamos que produzimos um objeto vazio:
deixe obj = {}; alerta(obj); // "[objeto Objeto]" ?
Onde está o código que gera a string "[object Object]"
? Esse é um método toString
integrado, mas onde está? O obj
está vazio!
…Mas a notação curta obj = {}
é a mesma que obj = new Object()
, onde Object
é uma função construtora de objeto integrada, com seu próprio prototype
referenciando um objeto enorme com toString
e outros métodos.
Aqui está o que está acontecendo:
Quando new Object()
é chamado (ou um objeto literal {...}
é criado), o [[Prototype]]
dele é definido como Object.prototype
de acordo com a regra que discutimos no capítulo anterior:
Então, quando obj.toString()
é chamado, o método é obtido de Object.prototype
.
Podemos verificar assim:
deixe obj = {}; alerta(obj.__proto__ === Object.prototype); // verdadeiro alerta(obj.toString === obj.__proto__.toString); //verdadeiro alerta(obj.toString === Object.prototype.toString); //verdadeiro
Observe que não há mais [[Prototype]]
na cadeia acima de Object.prototype
:
alerta(Object.prototype.__proto__); // nulo
Outros objetos embutidos como Array
, Date
, Function
e outros também mantêm métodos em protótipos.
Por exemplo, quando criamos um array [1, 2, 3]
, o construtor padrão new Array()
é usado internamente. Então Array.prototype
se torna seu protótipo e fornece métodos. Isso é muito eficiente em termos de memória.
Por especificação, todos os protótipos integrados possuem Object.prototype
na parte superior. É por isso que algumas pessoas dizem que “tudo herda dos objetos”.
Aqui está a imagem geral (para 3 integrados caberem):
Vamos verificar os protótipos manualmente:
deixe arr = [1, 2, 3]; // herda de Array.prototype? alerta( arr.__proto__ === Array.prototype ); // verdadeiro // então de Object.prototype? alerta( arr.__proto__.__proto__ === Object.prototype ); // verdadeiro // e nulo no topo. alerta( arr.__proto__.__proto__.__proto__ ); // nulo
Alguns métodos em protótipos podem se sobrepor, por exemplo, Array.prototype
tem seu próprio toString
que lista elementos delimitados por vírgula:
deixe arr = [1, 2, 3] alerta(arr); // 1,2,3 <- o resultado de Array.prototype.toString
Como vimos antes, Object.prototype
também tem toString
, mas Array.prototype
está mais próximo na cadeia, então a variante array é usada.
Ferramentas no navegador, como o console do desenvolvedor do Chrome, também mostram herança ( console.dir
pode precisar ser usado para objetos integrados):
Outros objetos integrados também funcionam da mesma maneira. Até funções – são objetos de um construtor Function
integrado, e seus métodos ( call
/ apply
e outros) são retirados de Function.prototype
. As funções também têm seu próprio toString
.
função f() {} alerta(f.__proto__ == Função.protótipo); // verdadeiro alerta(f.__proto__.__proto__ == Object.prototype); // verdadeiro, herda de objetos
O mais complicado acontece com strings, números e booleanos.
Como lembramos, eles não são objetos. Mas se tentarmos acessar suas propriedades, objetos wrapper temporários serão criados usando construtores integrados String
, Number
e Boolean
. Eles fornecem os métodos e desaparecem.
Esses objetos são criados de forma invisível para nós e a maioria dos motores os otimiza, mas a especificação descreve exatamente desta forma. Os métodos desses objetos também residem em protótipos, disponíveis como String.prototype
, Number.prototype
e Boolean.prototype
.
Valores null
e undefined
não possuem wrappers de objeto
Valores especiais null
e undefined
são separados. Eles não possuem wrappers de objetos, portanto métodos e propriedades não estão disponíveis para eles. E também não há protótipos correspondentes.
Protótipos nativos podem ser modificados. Por exemplo, se adicionarmos um método a String.prototype
, ele ficará disponível para todas as strings:
String.prototype.show=função() { alerta(este); }; "BOOM!".show(); // BUM!
Durante o processo de desenvolvimento, podemos ter ideias para novos métodos integrados que gostaríamos de ter e podemos ficar tentados a adicioná-los a protótipos nativos. Mas isso geralmente é uma má ideia.
Importante:
Os protótipos são globais, por isso é fácil criar conflitos. Se duas bibliotecas adicionarem um método String.prototype.show
, uma delas substituirá o método da outra.
Portanto, geralmente, modificar um protótipo nativo é considerado uma má ideia.
Na programação moderna, há apenas um caso em que a modificação de protótipos nativos é aprovada. Isso é polipreenchimento.
Polyfilling é um termo para substituir um método que existe na especificação JavaScript, mas ainda não é suportado por um mecanismo JavaScript específico.
Podemos então implementá-lo manualmente e preencher o protótipo integrado com ele.
Por exemplo:
if (!String.prototype.repeat) { // se tal método não existir //adiciona-o ao protótipo String.prototype.repeat = função (n) { //repete a string n vezes //na verdade, o código deveria ser um pouco mais complexo que isso // (o algoritmo completo está na especificação) // mas mesmo um polyfill imperfeito é frequentemente considerado bom o suficiente retornar novo Array(n + 1).join(this); }; } alerta("La".repeat(3) ); //LaLaLa
No capítulo Decoradores e encaminhamento, ligue/aplique falamos sobre empréstimo de métodos.
É quando pegamos um método de um objeto e o copiamos para outro.
Alguns métodos de protótipos nativos são frequentemente emprestados.
Por exemplo, se estivermos criando um objeto semelhante a um array, podemos querer copiar alguns métodos Array
para ele.
Por exemplo
deixe obj = { 0: "Olá", 1: "mundo!", comprimento: 2, }; obj.join = Array.prototype.join; alerta( obj.join(',') ); // Olá, mundo!
Funciona porque o algoritmo interno do método join
integrado se preocupa apenas com os índices corretos e a propriedade length
. Não verifica se o objeto é realmente um array. Muitos métodos integrados são assim.
Outra possibilidade é herdar definindo obj.__proto__
como Array.prototype
, para que todos os métodos Array
estejam automaticamente disponíveis em obj
.
Mas isso é impossível se obj
já herda de outro objeto. Lembre-se, só podemos herdar de um objeto por vez.
Os métodos de empréstimo são flexíveis e permitem misturar funcionalidades de diferentes objetos, se necessário.
Todos os objetos incorporados seguem o mesmo padrão:
Os métodos são armazenados no protótipo ( Array.prototype
, Object.prototype
, Date.prototype
, etc.)
O próprio objeto armazena apenas os dados (itens da matriz, propriedades do objeto, data)
Os primitivos também armazenam métodos em protótipos de objetos wrapper: Number.prototype
, String.prototype
e Boolean.prototype
. Apenas undefined
e null
não possuem objetos wrapper
Os protótipos integrados podem ser modificados ou preenchidos com novos métodos. Mas não é recomendado alterá-los. O único caso permitido é provavelmente quando adicionamos um novo padrão, mas ele ainda não é suportado pelo mecanismo JavaScript
importância: 5
Adicione ao protótipo de todas as funções o método defer(ms)
, que executa a função após ms
milissegundos.
Depois de fazer isso, esse código deve funcionar:
função f() { alerta("Olá!"); } f.defer(1000); // mostra "Olá!" depois de 1 segundo
Função.prototype.defer=função(ms){ setTimeout(isto,ms); }; função f() { alerta("Olá!"); } f.defer(1000); // mostra "Olá!" depois de 1 segundo
importância: 4
Adicione ao protótipo de todas as funções o método defer(ms)
, que retorna um wrapper, atrasando a chamada em ms
milissegundos.
Aqui está um exemplo de como deve funcionar:
função f(a, b) { alerta(a + b); } f.defer(1000)(1, 2); // mostra 3 após 1 segundo
Observe que os argumentos devem ser passados para a função original.
Função.prototype.defer=função(ms){ seja f = isto; função de retorno(...args) { setTimeout(() => f.apply(this, args), ms); } }; //verifique função f(a, b) { alerta(a + b); } f.defer(1000)(1, 2); // mostra 3 após 1 segundo
Observação: usamos this
em f.apply
para fazer nossa decoração funcionar para métodos de objetos.
Portanto, se a função wrapper for chamada como um método de objeto, this
será passado para o método original f
.
Função.prototype.defer=função(ms){ seja f = isto; função de retorno(...args) { setTimeout(() => f.apply(this, args), ms); } }; deixe usuário = { nome: "João", digaOi() { alerta(este.nome); } } usuário.sayHi = usuário.sayHi.defer(1000); user.sayHi();