Dia 9 — Objetos e desestruturação
Objetos: propriedades, métodos e o this
Objetos: propriedades, métodos e o this
Um objeto é uma coleção de pares chave e valor. As propriedades são os
valores nomeados; o método é uma propriedade que guarda função. Em React
Native, objeto é o formato de quase todo dado que chega da API e o formato do
estado de um componente.
const usuario = { nome: 'Ana', idade: 30 }; console.log(usuario.nome); // 'Ana'
O objeto literal é o que está entre {}: é a forma curta, e é a que se usa
sempre. A notação de ponto é usuario.nome; a notação de colchete é
usuario['nome'], e as duas dão o mesmo resultado. O colchete é o necessário
quando o nome da propriedade vem de uma variável:
const campo = 'idade'; console.log(usuario[campo]); // 30
O exemplo do objeto tem quatro propriedades — nome, idade, ativo e o
método saudar — e a primeira linha sai ponto: Ana | colchete: Ana: as duas
notações na mesma linha, com o mesmo resultado. A linha seguinte usa a variável
no colchete e sai colchete com variavel: 30, que é a razão de o colchete
existir: com o ponto, usuario.campo leria a propriedade chamada campo, e
não a que está dentro da variável.
O objeto da API tem erro: null ao lado dos dados, e isso aparece no fim do
exemplo: da api: 1 tarefa | erro: null. A mesma resposta traz o resultado e o
motivo de não ter resultado, e é assim que o app decide o que desenhar.
Propriedade que não existe
Ler uma propriedade ausente devolve undefined — não é erro. Escrever uma
propriedade nova cria a chave:
console.log(usuario.email); // undefined usuario.email = '[email protected]'; // cria a chave
O teste de existência tem duas formas, e a diferença importa:
console.log('nome' in usuario); // true console.log(usuario.email !== undefined); // false: a chave existe com valor undefined
A forma in pergunta pela chave; a comparação pergunta pelo valor. Chave
existente com valor undefined existe.
Para apagar, delete:
delete usuario.email;
As quatro linhas do exemplo mostram a propriedade ausente e as duas formas de
testá-la. email: undefined é a leitura que não é erro, e
depois de escrever: [email protected] é a chave criada pela atribuição. Depois,
'apelido' in objeto: true ao lado de apelido !== undefined: false — o mesmo
objeto com apelido: undefined, e as duas perguntas discordando.
O par é a diferença do parágrafo: a chave existe, o valor é undefined. Quem
quiser saber se pode usar o campo consulta o valor; quem quiser saber se a
chave foi removida consulta a chave.
O delete fecha o ciclo, e a saída é
depois do delete: [ 'nome', 'idade', 'ativo', 'saudar' ]. O email sumiu da
lista de chaves, e o que ficou é tudo o mais — inclusive saudar, o método.
É essa linha que mostra que o objeto tem quatro propriedades e não três, e que
delete removeu uma só.
Os três métodos que listam um objeto
Object.keys(usuario); // ['nome', 'idade'] Object.values(usuario); // ['Ana', 30] Object.entries(usuario); // [['nome', 'Ana'], ['idade', 30]]
Os três devolvem array, e por isso entram direto em map e filter. É assim
que a tela monta uma lista de linhas a partir de um objeto que veio da API:
Object.entries(usuario).map(([chave, valor]) => `${chave}: ${valor}`);
As três linhas do exemplo são keys: [ 'nome', 'idade', 'ativo', 'saudar' ],
values: [ 'Ana', 30, true, [Function: saudar] ] e o entries em quatro pares.
Repare que o método saudar aparece nos três: Object.keys devolve o nome dele,
Object.values devolve a função, e Object.entries devolve o par com os dois.
Quem quiser só os dados, sem método, precisa filtrar — e o filtro é por
typeof.
O entries com map do exemplo sai nome: Ana | idade: 30 sobre um objeto de
duas propriedades, e é a linha que mostra o uso da desestruturação do par:
([chave, valor]) são dois nomes recebidos do array de dois itens que cada
entrada é.
O this: o que é e quando existe
this é o objeto que chamou o método, e só existe dentro de uma função que foi
chamada como método:
const contador = { total: 0, somar(n) { this.total += n; return this.total; }, }; contador.somar(5); // 5
O this é o que permite o mesmo método servir para vários objetos: cada chamada
this é o objeto daquela chamada. Se o método for guardado numa variável e
chamado sem o objeto na frente — const somar = contador.somar; somar(1) —, o
this deixa de ser o contador. Em arquivo .js comum, o que acontece é pior que
erro: o this vira o objeto global, a conta sai NaN e nada acusa. O
contador original nem é tocado. Por isso a regra é chamar sempre com o objeto na
frente, ou usar a forma explícita contador.somar(1). Note que nem o objeto
original é tocado: o total continua sendo o das chamadas feitas com o objeto
na frente.
Esse é o motivo de a função seta não servir como método: a seta não tem this
próprio, e o this dela é o do lugar onde foi escrita. Para método que precisa
de this, usa-se function.
A cadeia de chamadas liga o método ao próximo: lista.map(item => item.feita)
chama map no array e a seta em cada item.
Chamar método inexistente é TypeError: lista.naoExiste is not a function, e
ler propriedade de null ou undefined é
TypeError: Cannot read properties of undefined. São os dois erros que mais
aparecem no console do aluno, e os dois dizem exatamente onde olhar.
O this do exemplo está em quatro linhas que contam a história inteira.
metodo: ola, Ana mostra o método lendo this.nome e devolvendo o texto.
this no metodo: 5 | outra chamada: 8 mostra duas somas no mesmo objeto: 5 e
depois 8, porque o this.total += n somou ao valor que já estava lá. A linha
o total continua o das chamadas feitas com o objeto: 8 prova que as duas
chamadas mexeram no mesmo contador.
Entre elas está a linha que é o defeito: metodo sem objeto na frente: NaN. O
método foi guardado em const somar e chamado sozinho, então o this não era o
contador — e o total de onde ele foi procurar era undefined. undefined += 1
é NaN, e o console mostra NaN sem TypeError e sem aviso. É o
silêncio do modo frouxo de novo, e por isso o NaN na tela é quase sempre um
this perdido.
A linha seta como metodo: sem this é o outro lado: a seta não consultou o
relogio, que tinha horas: 10 ao lado. Ela devolveu o texto fixo, e é isso que
a diferencia de function quando as duas estão escritas como método.
Os dois erros do fim do exemplo saem em `metodo inexistente: TypeError -
[1,2].naoExiste is not a function e propriedade de null: TypeError - Cannot
read properties of null (reading 'nome')`. O primeiro diz qual método não
existe; o segundo diz qual propriedade foi lida e em qual objeto — e a parte
(reading 'nome') é a que aponta a linha exata.
O objeto aninhado do exemplo é o caminho até o dado:
aninhado: Bia | com atalho: 4. O primeiro é usuarioCompleto.filhas.nome; o
segundo é usuarioCompleto.filhas?.idade, com o atalho de opcional. A
diferença aparece quando filhas não existe: o ponto dá TypeError e o atalho
devolve undefined.
Exemplo
// Objeto: propriedades, metodos e o this. O exemplo mostra a diferenca // entre notacao de ponto e de colchete, e os tres metodos que listam. const usuario = { nome: 'Ana', idade: 30, ativo: true, saudar() { return `ola, ${this.nome}`; }, }; /* notacao de ponto e de colchete dao o mesmo resultado */ console.log('ponto:', usuario.nome, '| colchete:', usuario['nome']); /* o colchete e necessario quando o nome vem de uma variavel */ const campo = 'idade'; console.log('colchete com variavel:', usuario[campo]); /* propriedade ausente devolve undefined, nao e erro */ console.log('email:', usuario.email); usuario.email = '[email protected]'; // cria a chave console.log('depois de escrever:', usuario.email); /* in pergunta pela chave; comparacao pergunta pelo valor */ const comIndefinido = { nome: 'Ana', apelido: undefined }; console.log("'apelido' in objeto:", 'apelido' in comIndefinido); console.log('apelido !== undefined:', comIndefinido.apelido !== undefined); /* delete remove a chave */ delete usuario.email; console.log('depois do delete:', Object.keys(usuario)); /* os tres metodos que listam */ console.log('keys:', Object.keys(usuario)); console.log('values:', Object.values(usuario)); console.log('entries:', Object.entries(usuario)); console.log('entries com map:', Object.entries({ nome: 'Ana', idade: 30 }) .map(([chave, valor]) => `${chave}: ${valor}`).join(' | ')); /* metodo e propriedade que guarda funcao; e o this que liga os dois */ console.log('metodo:', usuario.saudar()); const contador = { total: 0, somar(n) { this.total += n; return this.total; }, }; console.log('this no metodo:', contador.somar(5), '| outra chamada:', contador.somar(3)); /* funcao seta nao tem this proprio: nao serve como metodo */ const relogio = { horas: 10, descrever: () => 'sem this' }; console.log('seta como metodo:', relogio.descrever()); /* metodo guardado e chamado sem o objeto na frente: o this vira o objeto global em arquivo .js, e a conta sai errada sem erro nenhum */ const somar = contador.somar; console.log('metodo sem objeto na frente:', somar(1)); console.log('o total continua o das chamadas feitas com o objeto:', contador.total); /* os dois erros mais comuns no console do aluno */ try { [1, 2].naoExiste(); } catch (erro) { console.log('metodo inexistente:', erro.constructor.name, '-', erro.message); } try { const usuarioVazio = null; console.log(usuarioVazio.nome); } catch (erro) { console.log('propriedade de null:', erro.constructor.name, '-', erro.message); } /* objeto aninhado: o caminho ate o dado */ const usuarioCompleto = { nome: 'Ana', filhas: { nome: 'Bia', idade: 4 } }; console.log('aninhado:', usuarioCompleto.filhas.nome, '| com atalho:', usuarioCompleto.filhas?.idade); /* o objeto que vem da API */ const resposta = { dados: { tarefas: [{ id: 1, texto: 'estudar' }] }, erro: null }; console.log('da api:', resposta.dados.tarefas.length, 'tarefa | erro:', resposta.erro);
Saída real
ponto: Ana | colchete: Ana colchete com variavel: 30 email: undefined depois de escrever: [email protected] 'apelido' in objeto: true apelido !== undefined: false depois do delete: [ 'nome', 'idade', 'ativo', 'saudar' ] keys: [ 'nome', 'idade', 'ativo', 'saudar' ] values: [ 'Ana', 30, true, [Function: saudar] ] entries: [ [ 'nome', 'Ana' ], [ 'idade', 30 ], [ 'ativo', true ], [ 'saudar', [Function: saudar] ] ] entries com map: nome: Ana | idade: 30 metodo: ola, Ana this no metodo: 5 | outra chamada: 8 seta como metodo: sem this metodo sem objeto na frente: NaN o total continua o das chamadas feitas com o objeto: 8 metodo inexistente: TypeError - [1,2].naoExiste is not a function propriedade de null: TypeError - Cannot read properties of null (reading 'nome') aninhado: Bia | com atalho: 4 da api: 1 tarefa | erro: null
Desestruturação e spread
Desestruturação e spread
Desestruturação é escrever o nome de várias variáveis na mesma declaração,
colchete para frente do valor. Em objeto a forma é const seguido de { } — o
termo é const { }, sem nada entre a palavra e a chave. Em
array é o colchete comum. É a forma mais curta de pegar campos de objeto e de
pegar itens de array:
const usuario = { nome: 'Ana', idade: 30 }; const { nome, idade } = usuario;
Equivale a const nome = usuario.nome; const idade = usuario.idade;. O que faz
diferença é o uso: em React Native quase todo componente começa desestruturando,
porque o padrão são muitas props.
A propriedade renomeada usa : entre o nome original e o nome novo:
const { nome: apelido } = usuario; // apelido vale 'Ana'
O nome depois do : é o nome da variável; o nome antes é o da propriedade.
O exemplo começa por dois campos: Ana 30, que é a linha do bloco acima: duas
variáveis nascidas de um objeto, e o console lê os dois nomes soltos. A linha do
nome trocado é renomeado: Ana — o valor é o mesmo, o nome da variável é outro.
E o valor padrão entra junto com a renomeação, numa linha só:
padrao: sem cidade | renomeado com padrao: 30. O objeto não tem cidade, e
o padrão entrou; o objeto tem idade, e o = 0 do padrão não foi usado porque
o valor existia. O = 0 apareceu depois do :, o que mostra que os dois
mecanismos se combinam na mesma declaração.
Desestruturar array
No array é por posição, e a ordem importa:
const tarefas = ['estudar', 'treinar']; const [primeira, segunda] = tarefas;
O que falta vira undefined, e o valor padrão (=) cobre o buraco:
const [a, b = 'padrao'] = ['so um'];
Ignorar a primeira posição é poner espaço: const [, segundo] = tarefas.
As três linhas do exemplo resolvem posição, buraco e posição pulada.
array: estudar treinar são os dois primeiros itens da lista de três, na ordem.
array com buraco: so um padrao é o array de um item só: o primeiro veio, e o
segundo não existia, então o padrão entrou no lugar. `terceira posicao:
descansar pula duas posições com dois colchetes vazios, [, , terceiro]`.
Um detalhe do [, , terceiro]: as vírgulas dizem quantas posições pular, e cada
vírgula vazia é uma posição que não se quer. Não existe atalho para pegar o
último de um array por posição — para isso existe o .at(-1) da aula de arrays.
Desestruturar objeto aninhado
Duas vezes o colchete, na mesma linha:
const pessoa = { nome: 'Ana', filhas: { nome: 'Bia', idade: 4 } }; const { nome, filhas: { nome: filha, idade } } = pessoa;
A linha do exemplo é a mesma regra com nomes que não colidem:
{ nome: nomeDaPessoa, filhas: { nome: filha, idade: idadeDaFilha } }, e a
saída é aninhado: Ana Bia 4. A renomeação não é um detalhe aqui: sem ela,
o nome de fora e o nome de dentro seriam o mesmo nome, e o segundo
sobrescreveria o primeiro.
Quando a camada interna pode não existir, o operador opcional ?. evita o
TypeError: pessoa.filhas?.nome devolve undefined em vez de quebrar. E o
valor padrão junto (filhas = {}) resolve o caso de a chave não existir.
O exemplo usa o atalho num objeto que não tem filhas e imprime
com optional chaining: undefined | com padrao: Bia. A primeira parte é o objeto
sem a camada interna: o ?. short-circuitou e devolveu undefined, sem erro.
A segunda é o objeto com a camada, e o ?. deixou passar o nome.
É a diferença entre as duas defesas do parágrafo: o ?. protege a leitura de
uma camada que pode faltar, e o = {} protege a desestruturação quando o
parâmetro inteiro não chegou. O primeiro devolve undefined; o segundo evita o
TypeError da desestruturação.
Spread: espalhar as propriedades
O spread (...) copia as propriedades de um objeto num objeto novo, ou os
itens de um array num array novo:
const base = { nome: 'Ana' }; const completa = { ...base, idade: 30 };
A ordem importa: o que vem depois sobrescreve. { ...base, nome: 'Bia' } tem
'Bia'; { nome: 'Bia', ...base } tem 'Ana'. Por isso o spread entra no
começo quando o novo objeto é variação do antigo.
Em array, o spread achata um array dentro de outro:
const a = [1, 2]; const b = [...a, 3]; // [1, 2, 3]
Object.assign(alvo, fonte) faz o mesmo que o spread, e é a forma antiga:
Object.assign({}, base, { idade: 30 });
Copiar objeto sem Object.assign é o spread. A diferença que importa na
prática: Object.assign **mexe no primeiro
argumento**, e o spread cria objeto novo. Object.assign(base, { idade: 30 })
altera base.
A ordem do parágrafo sai numa linha do exemplo:
spread antes: Bia | spread depois: Ana. Com o ...base primeiro, o 'Bia'
escrito depois vence; com o 'Bia' primeiro e o ...base depois, o 'Ana' da
base vence. Nenhum dos dois objetos foi alterado — as duas expressões existem
só dentro do console.log.
O spread de objeto sai em `spread: { nome: 'Ana', idade: 30 } | base continua:
{ nome: 'Ana' }: o objeto novo tem as duas propriedades e a base` ficou só com
o nome. É a prova de que o spread copia.
E o Object.assign aparece nas duas formas na mesma seção:
assign em objeto novo: { nome: 'Ana', idade: 30 } e
assign no proprio objeto: { nome: 'Ana', idade: 30 }. As duas linhas parecem
iguais e não são: a primeira passou {} como alvo e o resultado foi num objeto
descartável; a segunda passou alvo e o alvo ficou com a idade. É o defeito
do assign que a linha do parágrafo descreve — e a razão de o spread ter
substituído a forma antiga.
O par entre chaves e ...: o resto
O ... com o nome do resto é o mesmo operador no outro sentido: junta o que
sobrou num objeto ou array novo.
const { nome, ...resto } = usuario; // nome vale 'Ana', resto tem o resto das propriedades const [primeiro, ...outros] = [1, 2, 3]; // primeiro vale 1, outros vale [2, 3]
É a forma de um componente aceitar quase tudo, repassar quase tudo, e mexer só em
um campo:
function Botao({ texto, ...outrasProps }) { return <Pressable {...outrasProps}><Text>{texto}</Text></Pressable>; }
As duas linhas do exemplo são rest em objeto: { idade: 30, ativo: true } e
rest em array: [ 'treinar', 'descansar' ]. O objeto tinha nome, idade e
ativo, e o resto ficou com os dois últimos; o array tinha três textos, e o
outros ficou com os dois últimos. O que foi nomeado sai da lista, e o resto
fica inteiro.
O uso real do rest sai em
repasse: {"texto":"Salvar","repassadas":["onPress","testID"]}. A função pegou
texto e deixou o resto; o resultado tem texto à parte e o Object.keys do
resto mostra o que sobrou. É exatamente o que o Botao do bloco acima faz com
outrasProps: uma prop usada por conta própria, todas as outras repassadas sem
nomes.
Cuidado: desestruturar perde o original
const { nome } = usuario não apaga nada — usuario.nome continua lá. Mas
desestruturar de um parâmetro que chega undefined quebra na hora, e o erro sai
antes da tela:
function mostrar({ nome }) { return nome; } mostrar(undefined); // TypeError
A linha do exemplo é `desestruturar undefined: TypeError - Cannot destructure
property 'nome' of 'undefined' as it is undefined.` A mensagem diz as três
coisas que importam: que a desestruturação falhou, qual propriedade foi pedida, e
que o valor de origem era undefined.
O conserto é a linha seguinte, e são duas formas na mesma linha:
com padrao duplo: visitante | com valor: Ana. A função tem
= {} no parâmetro e = 'visitante' na propriedade — o padrão do objeto
protege a desestruturação, e o padrão da propriedade protege a leitura. Chamada
sem argumento, devolveu 'visitante'; chamada com o nome, devolveu 'Ana'.
É a diferença entre as duas defesas do parágrafo, e vale escolher por ordem: o
= {} vem depois da desestruturação das chaves, porque é o que impede a
desestruturação de rodar em cima de undefined.
Exemplo
// Destruturacao e spread: pegar campos, renomear, achatar e repassar. const usuario = { nome: 'Ana', idade: 30, ativo: true }; const pessoa = { nome: 'Ana', filhas: { nome: 'Bia', idade: 4 } }; const tarefas = ['estudar', 'treinar', 'descansar']; /* destructuring de objeto: o nome das variaveis vem da chave */ const { nome, idade } = usuario; console.log('dois campos:', nome, idade); /* renomeando: o nome depois dos dois pontos e o da variavel */ const { nome: apelido } = usuario; console.log('renomeado:', apelido); /* valor padrao cobre o buraco */ const { cidade = 'sem cidade', idade: anos = 0 } = usuario; console.log('padrao:', cidade, '| renomeado com padrao:', anos); /* destructuring de array: por posicao */ const [primeira, segunda] = tarefas; console.log('array:', primeira, segunda); /* o que falta vira undefined; o padrao cobre */ const [um, dois = 'padrao'] = ['so um']; console.log('array com buraco:', um, dois); /* ignorar a primeira posicao */ const [, , terceiro] = tarefas; console.log('terceira posicao:', terceiro); /* objeto aninhado em duas linhas de colchete */ const { nome: nomeDaPessoa, filhas: { nome: filha, idade: idadeDaFilha } } = pessoa; console.log('aninhado:', nomeDaPessoa, filha, idadeDaFilha); /* ?. e o padrao quando a camada interna nao existe */ const semFilhas = { nome: 'Bruno' }; console.log('com optional chaining:', semFilhas.filhas?.nome, '| com padrao:', pessoa.filhas?.nome); /* spread de objeto: cria um objeto novo, e o original fica como estava */ const base = { nome: 'Ana' }; const completa = { ...base, idade: 30 }; console.log('spread:', completa, '| base continua:', base); /* a ordem decide quem vence */ console.log('spread antes:', { ...base, nome: 'Bia' }.nome, '| spread depois:', { nome: 'Bia', ...base }.nome); /* spread de array: achata */ const a = [1, 2]; console.log('achata:', [...a, 3], '| dentro de outro:', [...a, ...[4, 5]]); /* Object.assign faz o mesmo, mas mexe no primeiro argumento */ console.log('assign em objeto novo:', Object.assign({}, base, { idade: 30 })); const alvo = { nome: 'Ana' }; Object.assign(alvo, { idade: 30 }); console.log('assign no proprio objeto:', alvo); /* rest: o que sobra */ const { nome: primeiroNome, ...resto } = usuario; console.log('rest em objeto:', resto); const [primeiro, ...outros] = tarefas; console.log('rest em array:', outros); /* o componente que repassa quase tudo: o uso real do rest */ function resumoDe(props) { const { texto, ...outras } = props; return { texto, repassadas: Object.keys(outras) }; } console.log('repasse:', JSON.stringify(resumoDe({ texto: 'Salvar', onPress: () => {}, testID: 'botao' }))); /* desestruturar de quem chegou undefined quebra na hora */ try { (function ({ nome }) { return nome; })(undefined); } catch (erro) { console.error(erro.constructor.name + ': ' + erro.message); console.log('desestruturar undefined:', erro.constructor.name, '-', erro.message); } /* o padrao resolve o mesmo caso */ function comPadrao({ nome = 'visitante' } = {}) { return nome; } console.log('com padrao duplo:', comPadrao(), '| com valor:', comPadrao({ nome: 'Ana' }));
Saída real
dois campos: Ana 30
renomeado: Ana
padrao: sem cidade | renomeado com padrao: 30
array: estudar treinar
array com buraco: so um padrao
terceira posicao: descansar
aninhado: Ana Bia 4
com optional chaining: undefined | com padrao: Bia
spread: { nome: 'Ana', idade: 30 } | base continua: { nome: 'Ana' }
spread antes: Bia | spread depois: Ana
achata: [ 1, 2, 3 ] | dentro de outro: [ 1, 2, 4, 5 ]
assign em objeto novo: { nome: 'Ana', idade: 30 }
assign no proprio objeto: { nome: 'Ana', idade: 30 }
rest em objeto: { idade: 30, ativo: true }
rest em array: [ 'treinar', 'descansar' ]
repasse: {"texto":"Salvar","repassadas":["onPress","testID"]}
desestruturar undefined: TypeError - Cannot destructure property 'nome' of 'undefined' as it is undefined.
com padrao duplo: visitante | com valor: Ana