Como sabemos, os objetos podem armazenar propriedades.
Até agora, uma propriedade era um simples par de “valor-chave” para nós. Mas uma propriedade de objeto é, na verdade, algo mais flexível e poderoso.
Neste capítulo estudaremos opções de configuração adicionais e no próximo veremos como transformá-las invisivelmente em funções getter/setter.
As propriedades do objeto, além de um value
, possuem três atributos especiais (os chamados “flags”):
writable
– se true
, o valor pode ser alterado, caso contrário, será somente leitura.
enumerable
– se true
, então listado em loops, caso contrário não listado.
configurable
– se true
, a propriedade pode ser excluída e esses atributos podem ser modificados, caso contrário não.
Ainda não os vimos, porque geralmente eles não aparecem. Quando criamos uma propriedade “da maneira usual”, todas elas são true
. Mas também podemos alterá-los a qualquer momento.
Primeiro, vamos ver como obter esses sinalizadores.
O método Object.getOwnPropertyDescriptor permite consultar todas as informações sobre uma propriedade.
A sintaxe é:
deixe descritor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
O objeto do qual obter informações.
propertyName
O nome da propriedade.
O valor retornado é o chamado objeto “descritor de propriedade”: ele contém o valor e todos os sinalizadores.
Por exemplo:
deixe usuário = { nome: "João" }; deixe descritor = Object.getOwnPropertyDescriptor(usuário, 'nome'); alerta( JSON.stringify(descritor, null, 2 ) ); /* descritor de propriedade: { "valor": "João", "gravável": verdadeiro, "enumerável": verdadeiro, "configurável": verdadeiro } */
Para alterar os sinalizadores, podemos usar Object.defineProperty.
A sintaxe é:
Object.defineProperty(obj, propertyName, descritor)
obj
, propertyName
O objeto e sua propriedade para aplicar o descritor.
descriptor
Objeto descritor de propriedade a ser aplicado.
Se a propriedade existir, defineProperty
atualiza seus sinalizadores. Caso contrário, ele cria a propriedade com o valor e sinalizadores fornecidos; nesse caso, se um sinalizador não for fornecido, será assumido false
.
Por exemplo, aqui um name
de propriedade é criado com todos os sinalizadores falsos:
deixe usuário = {}; Object.defineProperty(usuário, "nome", { valor: "João" }); deixe descritor = Object.getOwnPropertyDescriptor(usuário, 'nome'); alerta( JSON.stringify(descritor, null, 2 ) ); /* { "valor": "João", "gravável": falso, "enumerável": falso, "configurável": falso } */
Compare-o com user.name
“normalmente criado” acima: agora todos os sinalizadores são falsos. Se não é isso que queremos, é melhor defini-los como true
no descriptor
.
Agora vamos ver os efeitos dos sinalizadores por exemplo.
Vamos tornar user.name
não gravável (não pode ser reatribuído) alterando o sinalizador writable
:
deixe usuário = { nome: "João" }; Object.defineProperty(usuário, "nome", { gravável: falso }); usuário.nome = "Pete"; // Erro: Não é possível atribuir à propriedade somente leitura 'nome'
Agora ninguém pode alterar o nome do nosso usuário, a menos que aplique seu próprio defineProperty
para substituir o nosso.
Erros aparecem apenas no modo estrito
No modo não estrito, nenhum erro ocorre ao gravar em propriedades não graváveis e coisas assim. Mas a operação ainda não terá sucesso. As ações que violam a bandeira são simplesmente ignoradas silenciosamente e de forma não estrita.
Aqui está o mesmo exemplo, mas a propriedade é criada do zero:
deixe usuário = {}; Object.defineProperty(usuário, "nome", { valor: "João", // para novas propriedades precisamos listar explicitamente o que é verdadeiro enumerável: verdadeiro, configurável: verdadeiro }); alerta(usuário.nome); // John usuário.nome = "Pete"; // Erro
Agora vamos adicionar um toString
personalizado a user
.
Normalmente, um toString
integrado para objetos não é enumerável e não aparece em for..in
. Mas se adicionarmos um toString
próprio, por padrão ele aparecerá for..in
, assim:
deixe usuário = { nome: "João", toString() { retorne este.nome; } }; // Por padrão, ambas as nossas propriedades estão listadas: for (deixar digitar o usuário) alert(key); // nome, toString
Se não gostarmos, podemos definir enumerable:false
. Então ele não aparecerá em um loop for..in
, assim como o integrado:
deixe usuário = { nome: "João", toString() { retorne este.nome; } }; Object.defineProperty(usuário, "toString", { enumerável: falso }); // Agora nosso toString desaparece: for (deixar digitar o usuário) alert(key); // nome
Propriedades não enumeráveis também são excluídas de Object.keys
:
alerta(Object.keys(usuário)); // nome
O sinalizador não configurável ( configurable:false
) às vezes é predefinido para objetos e propriedades integrados.
Uma propriedade não configurável não pode ser excluída e seus atributos não podem ser modificados.
Por exemplo, Math.PI
não é gravável, não enumerável e não configurável:
deixe descritor = Object.getOwnPropertyDescriptor(Math, 'PI'); alerta( JSON.stringify(descritor, null, 2 ) ); /* { "valor": 3,141592653589793, "gravável": falso, "enumerável": falso, "configurável": falso } */
Portanto, um programador não consegue alterar o valor de Math.PI
ou substituí-lo.
Matemática.PI = 3; // Erro, pois possui gravável: false // excluir Math.PI também não funcionará
Também não podemos alterar Math.PI
para ser writable
novamente:
// Erro, por causa do configurável: false Object.defineProperty(Math, "PI", { gravável: verdadeiro });
Não há absolutamente nada que possamos fazer com Math.PI
.
Tornar uma propriedade não configurável é um caminho de mão única. Não podemos alterá-lo novamente com defineProperty
.
Atenção: configurable: false
evita alterações de flags de propriedade e sua exclusão, ao mesmo tempo que permite alterar seu valor.
Aqui user.name
não é configurável, mas ainda podemos alterá-lo (pois é gravável):
deixe usuário = { nome: "João" }; Object.defineProperty(usuário, "nome", { configurável: falso }); usuário.nome = "Pete"; //funciona bem excluir nome do usuário; // Erro
E aqui tornamos user.name
uma constante “selada para sempre”, assim como o Math.PI
integrado:
deixe usuário = { nome: "João" }; Object.defineProperty(usuário, "nome", { gravável: falso, configurável: falso }); // não será possível alterar user.name ou seus sinalizadores // tudo isso não funcionará: usuário.nome = "Pete"; excluir nome do usuário; Object.defineProperty(usuário, "nome", { valor: "Pete" });
A única alteração de atributo possível: gravável verdadeiro → falso
Há uma pequena exceção sobre a mudança de sinalizadores.
Podemos alterar writable: true
para false
para uma propriedade não configurável, evitando assim a modificação de seu valor (para adicionar outra camada de proteção). Mas não o contrário.
Existe um método Object.defineProperties(obj, descriptors) que permite definir muitas propriedades de uma vez.
A sintaxe é:
Object.defineProperties(obj, { prop1: descritor1, prop2: descritor2 // ... });
Por exemplo:
Object.defineProperties(usuário, { nome: {valor: "John", gravável: falso }, sobrenome: {valor: "Smith", gravável: falso }, // ... });
Assim, podemos definir muitas propriedades de uma vez.
Para obter todos os descritores de propriedades de uma vez, podemos usar o método Object.getOwnPropertyDescriptors(obj).
Junto com Object.defineProperties
ele pode ser usado como uma forma “com reconhecimento de sinalizadores” de clonar um objeto:
deixe clonar = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
Normalmente quando clonamos um objeto, usamos uma atribuição para copiar propriedades, assim:
for (deixe digitar o usuário) { clone[chave] = usuário[chave] }
…Mas isso não copia sinalizadores. Portanto, se quisermos um clone “melhor”, então Object.defineProperties
é o preferido.
Outra diferença é que for..in
ignora propriedades simbólicas e não enumeráveis, mas Object.getOwnPropertyDescriptors
retorna todos os descritores de propriedades, incluindo os simbólicos e não enumeráveis.
Os descritores de propriedades funcionam no nível de propriedades individuais.
Existem também métodos que limitam o acesso a todo o objeto:
Object.preventExtensions(obj)
Proíbe a adição de novas propriedades ao objeto.
Objeto.selo(obj)
Proíbe adição/remoção de propriedades. Define configurable: false
para todas as propriedades existentes.
Objeto.freeze(obj)
Proíbe adição/remoção/alteração de propriedades. Define configurable: false, writable: false
para todas as propriedades existentes.
E também existem testes para eles:
Object.isExtensible(obj)
Retorna false
se adicionar propriedades for proibido, caso contrário, true
.
Object.isSealed(obj)
Retorna true
se adicionar/remover propriedades for proibido e todas as propriedades existentes forem configurable: false
.
Object.isFrozen(obj)
Retorna true
se adicionar/remover/alterar propriedades for proibido e todas as propriedades atuais forem configurable: false, writable: false
.
Esses métodos raramente são usados na prática.