No primeiro capítulo desta seção, mencionamos que existem métodos modernos para configurar um protótipo.
Definir ou ler o protótipo com obj.__proto__
é considerado desatualizado e um tanto obsoleto (movido para o chamado “Anexo B” do padrão JavaScript, destinado apenas a navegadores).
Os métodos modernos para obter/definir um protótipo são:
Object.getPrototypeOf(obj) – retorna o [[Prototype]]
de obj
.
Object.setPrototypeOf(obj, proto) – define o [[Prototype]]
de obj
como proto
.
O único uso de __proto__
, que não é desaprovado, é como propriedade ao criar um novo objeto: { __proto__: ... }
.
Embora também exista um método especial para isso:
Object.create(proto[, descriptors]) – cria um objeto vazio com determinado proto
como [[Prototype]]
e descritores de propriedades opcionais.
Por exemplo:
deixe animal = { come: verdade }; // cria um novo objeto com animal como protótipo deixe coelho = Object.create(animal); // igual a {__proto__: animal} alerta(coelho.eats); // verdadeiro alert(Object.getPrototypeOf(coelho) === animal); // verdadeiro Object.setPrototypeOf(coelho, {}); // altera o protótipo do coelho para {}
O método Object.create
é um pouco mais poderoso, pois possui um segundo argumento opcional: descritores de propriedade.
Podemos fornecer propriedades adicionais para o novo objeto, assim:
deixe animal = { come: verdade }; deixe coelho = Object.create(animal, { salta: { valor: verdadeiro } }); alerta(coelho.jumps); // verdadeiro
Os descritores estão no mesmo formato descrito no capítulo Sinalizadores e descritores de propriedade.
Podemos usar Object.create
para realizar uma clonagem de objetos mais poderosa do que copiar propriedades em for..in
:
deixe clonar = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
Esta chamada faz uma cópia verdadeiramente exata de obj
, incluindo todas as propriedades: enumeráveis e não enumeráveis, propriedades de dados e setters/getters – tudo, e com o [[Prototype]]
certo.
Existem muitas maneiras de gerenciar [[Prototype]]
. Como isso aconteceu? Por que?
Isso é por razões históricas.
A herança prototípica estava na linguagem desde os seus primórdios, mas as formas de gerenciá-la evoluíram ao longo do tempo.
A propriedade prototype
de uma função construtora funciona desde tempos muito antigos. É a forma mais antiga de criar objetos com um determinado protótipo.
Mais tarde, no ano de 2012, Object.create
apareceu no padrão. Ele deu a capacidade de criar objetos com um determinado protótipo, mas não forneceu a capacidade de obtê-lo/defini-lo. Alguns navegadores implementaram o acessador __proto__
não padrão que permitia ao usuário obter/definir um protótipo a qualquer momento, para dar mais flexibilidade aos desenvolvedores.
Posteriormente, no ano de 2015, Object.setPrototypeOf
e Object.getPrototypeOf
foram adicionados ao padrão, para executar a mesma funcionalidade de __proto__
. Como __proto__
foi implementado de fato em todos os lugares, ele foi meio obsoleto e foi para o Anexo B do padrão, ou seja: opcional para ambientes que não sejam de navegador.
Mais tarde, no ano de 2022, foi oficialmente permitido usar __proto__
em objetos literais {...}
(retirado do Anexo B), mas não como getter/setter obj.__proto__
(ainda no Anexo B).
Por que __proto__
foi substituído pelas funções getPrototypeOf/setPrototypeOf
?
Por que __proto__
foi parcialmente reabilitado e seu uso permitido em {...}
, mas não como getter/setter?
Essa é uma questão interessante, que exige que entendamos por que __proto__
é ruim.
E em breve teremos a resposta.
Não altere [[Prototype]]
em objetos existentes se a velocidade for importante
Tecnicamente, podemos obter/definir [[Prototype]]
a qualquer momento. Mas normalmente nós o configuramos apenas uma vez no momento da criação do objeto e não o modificamos mais: rabbit
herda de animal
, e isso não vai mudar.
E os mecanismos JavaScript são altamente otimizados para isso. Alterar um protótipo “on-the-fly” com Object.setPrototypeOf
ou obj.__proto__=
é uma operação muito lenta, pois quebra as otimizações internas para operações de acesso à propriedade do objeto. Portanto, evite-o, a menos que você saiba o que está fazendo ou a velocidade do JavaScript não importa totalmente para você.
Como sabemos, os objetos podem ser usados como matrizes associativas para armazenar pares chave/valor.
…Mas se tentarmos armazenar chaves fornecidas pelo usuário nele (por exemplo, um dicionário inserido pelo usuário), podemos ver uma falha interessante: todas as chaves funcionam bem, exceto "__proto__"
.
Confira o exemplo:
deixe obj = {}; deixe chave = prompt("Qual é a chave?", "__proto__"); obj[chave] = "algum valor"; alerta(obj[chave]); // [objeto Objeto], não "algum valor"!
Aqui, se o usuário digitar __proto__
, a atribuição na linha 4 será ignorada!
Isso certamente poderia ser surpreendente para quem não é desenvolvedor, mas bastante compreensível para nós. A propriedade __proto__
é especial: deve ser um objeto ou null
. Uma string não pode se tornar um protótipo. É por isso que uma atribuição de uma string para __proto__
é ignorada.
Mas não pretendíamos implementar tal comportamento, certo? Queremos armazenar pares chave/valor, e a chave chamada "__proto__"
não foi salva corretamente. Então isso é um bug!
Aqui as consequências não são terríveis. Mas em outros casos podemos estar armazenando objetos em vez de strings em obj
, e então o protótipo será de fato alterado. Como resultado, a execução irá dar errado de maneiras totalmente inesperadas.
O que é pior – geralmente os desenvolvedores nem pensam nessa possibilidade. Isso torna esses bugs difíceis de detectar e até mesmo os transforma em vulnerabilidades, especialmente quando JavaScript é usado no lado do servidor.
Coisas inesperadas também podem acontecer ao atribuir obj.toString
, pois é um método de objeto integrado.
Como podemos evitar esse problema?
Primeiro, podemos simplesmente passar a usar Map
para armazenamento em vez de objetos simples, então está tudo bem:
deixe mapa = novo Mapa(); deixe chave = prompt("Qual é a chave?", "__proto__"); map.set(chave, "algum valor"); alerta(map.get(chave)); // "algum valor" (conforme pretendido)
…Mas a sintaxe Object
costuma ser mais atraente, pois é mais concisa.
Felizmente, podemos usar objetos, porque os criadores da linguagem pensaram nesse problema há muito tempo.
Como sabemos, __proto__
não é uma propriedade de um objeto, mas uma propriedade acessadora de Object.prototype
:
Portanto, se obj.__proto__
for lido ou definido, o getter/setter correspondente será chamado a partir de seu protótipo e obterá/definirá [[Prototype]]
.
Como foi dito no início desta seção do tutorial: __proto__
é uma forma de acessar [[Prototype]]
, não é [[Prototype]]
em si.
Agora, se pretendemos usar um objeto como um array associativo e ficarmos livres de tais problemas, podemos fazer isso com um pequeno truque:
deixe obj = Object.create(null); // ou: obj = { __proto__: null } deixe chave = prompt("Qual é a chave?", "__proto__"); obj[chave] = "algum valor"; alerta(obj[chave]); // "algum valor"
Object.create(null)
cria um objeto vazio sem um protótipo ( [[Prototype]]
é null
):
Portanto, não há getter/setter herdado para __proto__
. Agora ele é processado como uma propriedade de dados regular, então o exemplo acima funciona corretamente.
Podemos chamar esses objetos de objetos “muito simples” ou de “dicionário puro”, porque eles são ainda mais simples que o objeto simples regular {...}
.
Uma desvantagem é que esses objetos não possuem métodos de objeto integrados, por exemplo, toString
:
deixe obj = Object.create(null); alerta(obj); // Erro (sem toString)
…Mas isso geralmente funciona para matrizes associativas.
Observe que a maioria dos métodos relacionados a objetos são Object.something(...)
, como Object.keys(obj)
– eles não estão no protótipo, então continuarão trabalhando em tais objetos:
deixe chineseDictionary = Object.create(null); chineseDictionary.hello = "你好"; chineseDictionary.bye = "再见"; alerta(Object.keys(chineseDictionary)); // olá, tchau
Para criar um objeto com o protótipo fornecido, use:
O Object.create
fornece uma maneira fácil de copiar superficialmente um objeto com todos os descritores:
deixe clonar = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
sintaxe literal: { __proto__: ... }
, permite especificar múltiplas propriedades
ou Object.create(proto[, descritores]), permite especificar descritores de propriedades.
Os métodos modernos para obter/definir o protótipo são:
Object.getPrototypeOf(obj) – retorna o [[Prototype]]
de obj
(o mesmo que __proto__
getter).
Object.setPrototypeOf(obj, proto) – define o [[Prototype]]
de obj
como proto
(o mesmo que __proto__
setter).
Obter/definir o protótipo usando o getter/setter __proto__
integrado não é recomendado, ele está agora no Anexo B da especificação.
Também cobrimos objetos sem protótipo, criados com Object.create(null)
ou {__proto__: null}
.
Esses objetos são usados como dicionários, para armazenar quaisquer chaves (possivelmente geradas pelo usuário).
Normalmente, os objetos herdam métodos integrados e __proto__
getter/setter de Object.prototype
, tornando as chaves correspondentes “ocupadas” e potencialmente causando efeitos colaterais. Com o protótipo null
, os objetos estão realmente vazios.
importância: 5
Existe um dictionary
de objetos, criado como Object.create(null)
, para armazenar quaisquer pares key/value
.
Adicione o método dictionary.toString()
a ele, que deve retornar uma lista de chaves delimitada por vírgulas. Seu toString
não deve aparecer for..in
sobre o objeto.
Veja como deve funcionar:
deixe dicionário = Object.create(null); // seu código para adicionar o método Dictionary.toString //adiciona alguns dados dicionário.apple = "Apple"; dicionário.__proto__ = "teste"; // __proto__ é uma chave de propriedade regular aqui // apenas apple e __proto__ estão no loop for(deixe digitar no dicionário) { alerta(chave); // "maçã", depois "__proto__" } // seu toString em ação alerta(dicionário); // "maçã,__proto__"
O método pode pegar todas as chaves enumeráveis usando Object.keys
e gerar sua lista.
Para tornar toString
não enumerável, vamos defini-lo usando um descritor de propriedade. A sintaxe de Object.create
nos permite fornecer um objeto com descritores de propriedades como segundo argumento.
deixe dicionário = Object.create(null, { toString: { // define a propriedade toString value() { // o valor é uma função retornar Object.keys(this).join(); } } }); dicionário.apple = "Apple"; dicionário.__proto__ = "teste"; // apple e __proto__ estão no loop for(deixe digitar no dicionário) { alerta(chave); // "maçã", depois "__proto__" } // lista de propriedades separadas por vírgula por toString alerta(dicionário); // "maçã,__proto__"
Quando criamos uma propriedade usando um descritor, seus sinalizadores são false
por padrão. Portanto, no código acima, dictionary.toString
não é enumerável.
Consulte o capítulo Sinalizadores e descritores de propriedade para revisão.
importância: 5
Vamos criar um novo objeto rabbit
:
function Coelho(nome) { este.nome = nome; } Rabbit.prototype.sayHi = function() { alerta(este.nome); }; deixe coelho = new Coelho("Coelho");
Essas chamadas fazem a mesma coisa ou não?
coelho.digaOi(); Rabbit.prototype.sayHi(); Object.getPrototypeOf(coelho).sayHi(); coelho.__proto__.sayHi();
A primeira chamada tem this == rabbit
, as outras têm this
igual a Rabbit.prototype
, porque na verdade é o objeto antes do ponto.
Portanto, apenas a primeira chamada mostra Rabbit
, as outras mostram undefined
:
function Coelho(nome) { este.nome = nome; } Rabbit.prototype.sayHi = function() { alerta(este.nome); } deixe coelho = new Coelho("Coelho"); coelho.digaOi(); // Coelho Rabbit.prototype.sayHi(); // indefinido Object.getPrototypeOf(coelho).sayHi(); // indefinido coelho.__proto__.sayHi(); // indefinido