Como sabemos, objetos podem armazenar propriedades.
Até agora, para nós, uma propriedade era um simples par "chave-valor". Mas uma propriedade de objeto é na verdade uma coisa mais flexível e poderosa.
Neste capítulo, nós vamos estudar opções de configuração adicional, e no próximo nós vamos ver como invisivelmente tornar elas em funções getter/setter.
Propriedades de objeto, além do valor
tem três atributos especiais (também chamados "sinalizadores"):
gravável
-- setrue
, o valor pode ser alterado, caso contrário, é apenas-leitura.enúmeravel
-- setrue
, então pode ser listado em loops, caso contrário, não pode.configurável
-- setrue
, a propriedade pode ser deletada e seus atributos modificados, caso contrário não.
Nós não vimos eles ainda, porque geralmente eles não aparecem. Quando criamos uma propriedade "do jeito comum", todos eles são true
. Mas nós também podemos mudá-los a qualquer hora.
Primeiro, vamos ver como obter esses sinalizadores.
O método Object.getOwnPropertyDescriptor nos permite consultar a informação completa sobre a propriedade.
A sintaxe é:
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
: O objeto do qual vamos obter a informação.
propertyName
: O nome da propriedade.
O valor retornado é também chamado de objeto "descritor de propriedade": ele contém o valor e todos os sinalizadores.
Por exemplo:
let user = {
name: "John"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* descritor de propriedade:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
Para mudar os sinalizadores, nós podemos usar o Object.defineProperty.
A sintaxe é:
Object.defineProperty(obj, propertyName, descriptor)
obj
, propertyName
: O objeto e a propriedade nos quais atuar.
descriptor
: Descritor de propriedade de objeto a aplicar.
Se a proprieade existe, defineProperty
atualiza seu sinalizador. Caso contrário, é criada uma propriedade com os sinalizadores setados; neste caso, se um sinalizador não é enviado, seu valor é assumido como false
.
Por exemplo, aqui a propriedade name
é criada com todos os sinalizadores falsos:
let user = {};
*!*
Object.defineProperty(user, "name", {
value: "John"
});
*/!*
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "John",
*!*
"writable": false,
"enumerable": false,
"configurable": false
*/!*
}
*/
Compare isso com o user.name
"criado normalmente" acima: agora todos os sinalizadores são falsos. Se não é isso que queremos, então é melhor setá-los como true
no descriptor
.
Agora vamos ver os efeitos dos sinalizadores, por exemplo:
Vamos deixar user.name
não-gravável (não pode ser reatribuído) alterando o sinalizador writable
:
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
*!*
writable: false
*/!*
});
*!*
user.name = "Pete"; // Error: Cannot assign to read only property 'name'...
*/!*
Agora, ninguém pode alterar o nome do nosso usuário, a não ser que eles apliquem seus próprios defineProperty
para sobrescrever o nosso.
No modo não-estrito, os erros não ocorrem quando gravando em propriedades não-graváveis, etc. Mas a operação ainda não terá sucesso. Ações que violam os sinalizadores são apenas ignoradas silenciosamentes em modo não-estrito.
Aqui está o mesmo exemplo, mas a propriedade é criada do zero.
let user = { };
Object.defineProperty(user, "name", {
*!*
value: "John",
// para novas proprieades, precisamos explicitamente listar o que é true
enumerable: true,
configurable: true
*/!*
});
alert(user.name); // John
user.name = "Alice"; // Erro
Agora, vamos adicionar um toString
customizado ao user
.
Normalmente, um toString
embutido em objetos é não-enumerável, e não aparece em for...in
. Mas se nós adicionarmos um toString
por nós mesmos, então por padrão ele aparece em for...in
, desta forma:
let user = {
name: "John",
toString() {
return this.name;
}
};
// Por padrão, ambas as nossas propriedades são listadas:
for (let key in user) alert(key); // name, toString
Se nós não gostarmos disso, então podemos setar enumerable:false
. Então ela não vai aparecer no loop for...in
, assim como as propriedades embutidas:
let user = {
name: "John",
toString() {
return this.name;
}
};
Object.defineProperty(user, "toString", {
*!*
enumerable: false
*/!*
});
*!*
// Agora nosso toString desaparece:
*/!*
for (let key in user) alert(key); // name
Propriedades não-enumeráveis também são excluídas de Object.keys
:
alert(Object.keys(user)); // name
O sinalizador não-configurável (configurable:false
) algumas vezes é predefinido por objetos e propriedades embutidas.
Uma propriedade não-configurável não pode ser deletada.
Por exemplo, Math.PI
é não-gravável, não-enumerável e não-configurável:
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": 3.141592653589793,
"writable": false,
"enumerable": false,
"configurable": false
}
*/
Então, um programador é impossibilitado de mudar o valor de Math.PI
ou sobrescrevê-lo.
Math.PI = 3; // Erro
// deletar Math.PI também não irá funcionar
Deixar uma propriedade não-configurável, é um caminho só de ida. Nós não podemos alterar isso novamente com defineProperty
.
Para ser preciso, não-configurabilidade impões várias restrições em defineProperty
:
- Não poder mudar o sinalizador
configurable
. - Não poder mudar o sinalizador
enumerable
. - Não poder mudar
writable: false
paratrue
(o contrário funciona). - Não poder mudar
get/set
por um acessador de propriedade (mas pode atribuí-los se ausente).
A ideia de "configurable: false" é para prevenir mudanças de sinalizadores de propriedades e sua eliminação, enquanto permite alterar seu valor.
Aqui user.name
é não-configurável, mas nós ainda podemos alterá-lo (pois é gravável):
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
configurable: false
});
user.name = "Pete"; // funciona corretamente
delete user.name; // Erro
E aqui nós deixamos user.name
uma constante "selada para sempre":
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false,
configurable: false
});
// não será possível alterar user.name ou seus sinalizadores
// nada disso irá funcionar
user.name = "Pete";
delete user.name;
Object.defineProperty(user, "name", { value: "Pete" });
Existe um método Object.defineProperties(obj, descriptors) que permite definir várias propriedades de uma vez.
A sintaxe é:
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
Por exemplo:
Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
Então, nós podemos setar várias propriedades de uma vez.
Para obter todos os sinalizadores de propriedade de uma vez, nós podemos usar o método Object.getOwnPropertyDescriptors(obj).
Juntamente com Object.defineProperties
isso pode ser usado como um jeito "consciente-de-sinalizadores" de clonar objetos:
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
Normalmente quando nós clonamos um objeto, nós usamos uma atribuição para copiar propriedades, desta forma:
for (let key in user) {
clone[key] = user[key]
}
...Mas isso não copia os sinalizadores. Então se nós quisermos um clone "melhor" então é preferível Object.defineProperties
.
Outra diferença é que for..in
ignora propriedades simbólicas, mas Object.getOwnPropertyDescriptors
returna todas as propriedades descritoras, incluindo as simbólicas.
Descritores de propriedade atuam no mesmo nível de propriedades individuais.
Também existem métodos que limitam o acesso ao objeto inteiro:
Object.preventExtensions(obj) : Proíbe a adição de novas propriedades ao objeto.
Object.seal(obj)
: Proíbe a adição/remoção de propriedades. Seta configurable: false
para todas as propriedades existentes.
Object.freeze(obj)
: Proíbe adicionar/remover/alterar propriedades. Seta configurable: false, writable: false
para todas as propriedades existentes.
E também existem testes para eles:
Object.isExtensible(obj)
: Retorna false
se a adição de propriedades é proibida, caso contrátio true
.
Object.isSealed(obj)
: Retorna true
se adição/remoção de propriedades são proibidas, e todas as propriedades existentes são configurable: false
.
Object.isFrozen(obj)
: Retorna true
se adição/remoção/alteração de propriedades são proibidas, e todas as propriedades atuais são configurable: false, writable: false
.
Estes métodos são raramentes usados na prática.