Existem dois tipos de propriedades de objeto.
O primeiro tipo são as propriedades dos dados . Já sabemos como trabalhar com eles. Todas as propriedades que usamos até agora eram propriedades de dados.
O segundo tipo de propriedade é algo novo. É uma propriedade do acessador . São essencialmente funções executadas para obter e definir um valor, mas parecem propriedades regulares para um código externo.
As propriedades do acessador são representadas pelos métodos “getter” e “setter”. Em um objeto literal eles são denotados por get
e set
:
deixe obj = { obter propNome() { // getter, o código executado ao obter obj.propName }, definir propNome(valor) { // setter, o código executado na configuração obj.propName = value } };
O getter funciona quando obj.propName
é lido, o setter – quando é atribuído.
Por exemplo, temos um objeto user
com name
e surname
:
deixe usuário = { nome: "João", sobrenome: "Smith" };
Agora queremos adicionar uma propriedade fullName
, que deve ser "John Smith"
. Claro, não queremos copiar e colar informações existentes, então podemos implementá-las como um acessador:
deixe usuário = { nome: "João", sobrenome: "Smith", obterNomeCompleto() { return `${este.nome} ${este.sobrenome}`; } }; alerta(usuário.nome completo); //João Smith
Do lado de fora, uma propriedade de acessador parece normal. Essa é a ideia das propriedades do acessador. Não chamamos user.fullName
como uma função, lemos normalmente: o getter é executado nos bastidores.
A partir de agora, fullName
possui apenas um getter. Se tentarmos atribuir user.fullName=
, haverá um erro:
deixe usuário = { obterNomeCompleto() { retornar `...`; } }; user.fullName = "Teste"; // Erro (a propriedade possui apenas um getter)
Vamos consertar adicionando um setter para user.fullName
:
deixe usuário = { nome: "João", sobrenome: "Smith", obterNomeCompleto() { return `${este.nome} ${este.sobrenome}`; }, definir nome completo(valor) { [este.nome, este.sobrenome] = valor.split(" "); } }; // set fullName é executado com o valor fornecido. user.fullName = "Alice Cooper"; alerta(usuário.nome); // Alice alerta(usuário.sobrenome); // Cooper
Como resultado, temos uma propriedade “virtual” fullName
. É legível e gravável.
Os descritores das propriedades do acessador são diferentes daqueles das propriedades de dados.
Para propriedades do acessador, não há value
ou writable
, mas existem funções get
e set
.
Ou seja, um descritor de acessador pode ter:
get
– uma função sem argumentos, que funciona quando uma propriedade é lida,
set
– uma função com um argumento, que é chamada quando a propriedade é definida,
enumerable
– o mesmo que para propriedades de dados,
configurable
– o mesmo que para propriedades de dados.
Por exemplo, para criar um acessador fullName
com defineProperty
, podemos passar um descritor com get
e set
:
deixe usuário = { nome: "João", sobrenome: "Smith" }; Object.defineProperty(usuário, 'nome completo', { pegar() { return `${este.nome} ${este.sobrenome}`; }, definir(valor) { [este.nome, este.sobrenome] = valor.split(" "); } }); alerta(usuário.nome completo); //João Smith for(deixar digitar o usuário) alert(chave); //nome, sobrenome
Observe que uma propriedade pode ser um acessador (possui métodos get/set
) ou uma propriedade de dados (possui um value
), não ambos.
Se tentarmos fornecer get
e value
no mesmo descritor, ocorrerá um erro:
// Erro: descritor de propriedade inválido. Object.defineProperty({}, 'prop', { pegar() { retornar 1 }, valor: 2 });
Getters/setters podem ser usados como wrappers sobre valores de propriedades “reais” para obter mais controle sobre as operações com eles.
Por exemplo, se quisermos proibir nomes muito curtos para user
, podemos ter um setter name
e manter o valor em uma propriedade separada _name
:
deixe usuário = { obter nome() { retorne isto._nome; }, definir nome(valor) { if (valor.comprimento <4) { alert("O nome é muito curto, precisa de pelo menos 4 caracteres"); retornar; } this._name = valor; } }; usuário.nome = "Pete"; alerta(usuário.nome); // Pete nome do usuário = ""; // O nome é muito curto...
Assim, o nome é armazenado na propriedade _name
, e o acesso é feito via getter e setter.
Tecnicamente, o código externo é capaz de acessar o nome diretamente usando user._name
. Mas existe uma convenção amplamente conhecida de que as propriedades que começam com um sublinhado "_"
são internas e não devem ser tocadas de fora do objeto.
Um dos grandes usos dos acessadores é que eles permitem assumir o controle sobre uma propriedade de dados “normal” a qualquer momento, substituindo-a por um getter e um setter e ajustando seu comportamento.
Imagine que começamos a implementar objetos de usuário usando propriedades de dados name
e age
:
function Usuário(nome, idade) { este.nome = nome; esta.idade = idade; } deixe joão = novo usuário("João", 25); alerta(joão.idade); //25
…Mas, mais cedo ou mais tarde, as coisas podem mudar. Em vez de age
podemos decidir armazenar birthday
, porque é mais preciso e conveniente:
function Usuário(nome, aniversário) { este.nome = nome; this.birthday = aniversário; } deixe john = new User("John", new Date(1992, 6, 1));
Agora, o que fazer com o código antigo que ainda usa a propriedade age
?
Podemos tentar encontrar todos esses locais e corrigi-los, mas isso leva tempo e pode ser difícil de fazer se esse código for usado por muitas outras pessoas. E além disso, age
é uma coisa legal de se ter em user
, certo?
Vamos mantê-lo.
Adicionar um getter para age
resolve o problema:
function Usuário(nome, aniversário) { este.nome = nome; this.birthday = aniversário; // a idade é calculada a partir da data atual e do aniversário Object.defineProperty(this, "idade", { pegar() { deixe hojeAno = new Date().getFullYear(); retornar hojeAno - this.birthday.getFullYear(); } }); } deixe john = new User("John", new Date(1992, 6, 1)); alerta(joão.aniversário); // aniversário está disponível alerta(joão.idade); // ...bem como a idade
Agora o código antigo também funciona e temos uma propriedade adicional interessante.