Dia 4 — Strings
Concatenar, interpolar e medir
Concatenar, interpolar e medir
Texto em JavaScript é string, e string tem uma diferença em relação aos outros
tipos: ela é usada para duas coisas ao mesmo tempo — dado e elemento de tela. O
nome que aparece no Text do React Native é uma string; o valor que se compara
para validar um campo também é. Por isso os métodos de string são os mais
chamados do app inteiro.
typeof 'Ana' devolve 'string', e typeof string é o typeof string, e uma string vazia é '' — ela tem
comprimento zero e vale false quando testada. Esse é o teste de campo
obrigatório mais usado:
if (nome.trim() === '') { /* campo vazio */ }
O exemplo confirma essa parte com string vazia: 0 | typeof: string: o
comprimento de '' é 0, e o tipo continua sendo string. Comprimento zero é o
que separa "campo vazio" de "campo com texto" — e o trim antes do teste é o
que separa "campo vazio" de "campo só com espaços".
Concatenar com + e interpolar com crase
Concatenar é juntar strings com o operador +. Interpolação é a forma melhor, e o que o professor chama de template literal
usa a crase (o acento grave) em vez de aspa:
const nome = 'Ana'; const idade = 30; // concatenar const texto1 = 'Ola, ' + nome + '!'; console.log(texto1); // Ola, Ana! // interpolar const texto2 = `Ola, ${nome}!`; console.log(texto2); // Ola, Ana!
O ${ } é o que separa a crase da aspa: dentro das chaves vai **expressão de
JavaScript**, não só texto. ${idade + 1} funciona, ${tarefas.length} também.
Usar crase em toda stringInterpolation evita a concatenação por + e mantém
código no lugar certo — o que importa em React Native é que string com +
misturado dentro de JSX costuma virar erro de sintaxe.
+ só concatena se algum lado for string; ${ } converte o que colocar dentro,
inclusive objeto. ${ } é o que transforma um número em texto na tela:
const total = 42; console.log(`${total}`); // '42' string
O exemplo mostra a conversão com o typeof dos dois lados:
typeof do numero: number | typeof interpolado: string. A mesma idade vale
30 como número e sai '30' como string depois de passar pela crase — e é essa
mudança de tipo que a tela precisa, porque o React Native desenha texto, não
número.
E os três usos do ${ } aparecem em três linhas, uma por tipo de conteúdo:
Ola, ${nome}! para a variável, no ano que vem fara ${idade + 1} anos para a
expressão aritmética, e tarefas: ${tarefas.length} para o método. A segunda é
a que mais importa, porque mostra que dentro das chaves pode ficar uma conta
inteira — ${idade + 1} devolve 31, com idade valendo 30.
Medir: length e os três trimmed
.length devolve o número de caracteres, e só existe em string e array:
console.log('Ana'.length); // 3 console.log('Ana'.trimStart().length); // 3
trim apaga os espaços do começo e do fim. trimStart apaga só do começo e
trimEnd só do fim. Os três devolvem uma string nova — a original não muda,
porque string é imutável. Nenhum dos três apaga espaço no meio do texto.
O lugar de trim no app é o campo que o usuário preenche. ' Ana ' não é igual
a 'Ana', e comparar os dois dá false sem motivo visível na tela:
console.log(' Ana '.trim() === 'Ana'); // true
O exemplo usa JSON.stringify para mostrar os espaços, porque o console
imprimiria ' Ana ' sem diferença visível — as aspas seriam a única pista. A
linha antes: " Ana " | depois: "Ana" mostra os dois lados do trim, e a
seguinte confirma a meia-regra com espaco no meio continua: " Ana Silva ": o
espaço entre o nome e o sobrenome continua lá, porque trim só mexe nas
pontas. A última do grupo é campo vazio apos trim: true — ' ' só com
espaços vira string vazia depois do trim, e é essa linha que implementa o teste
do começo da página.
Mudar caixa e cortar as pontas
toUpperCase e toLowerCase devolvem o texto em maiúsculas ou minúsculas. Como
string é imutável, o resultado precisa ser guardado — chamar o método e ignorar
o retorno não muda nada:
console.log('Ana'.toUpperCase()); // ANA console.log('Ana'.toLowerCase()); // ana let nome = 'Ana'; nome.toUpperCase(); console.log(nome); // Ana: o retorno foi descartado nome = nome.toUpperCase(); console.log(nome); // ANA
O exemplo da página faz a demonstração do descarte em duas linhas, e o console
distingue as duas: descartando o retorno: Ana e, depois da atribuição,
guardando o retorno: ANA. A variável usuario é a mesma nas duas linhas — o
que muda é se o retorno do método foi usado.
Esse detalhe é a origem de um bug frequente em campo de busca: map com
toLowerCase() sem atribuir de volta não filtra nada, porque cada item
continua com o valor antigo.
O par final fecha com o filtro. As duas linhas do exemplo saem iguais —
filtrando sem atribuar: 2 e filtrando e guardando: 2 item — porque o
filter devolve uma lista nova e o .length conta os itens dessa lista. A
lista tarefas do arquivo tem três itens, e dois deles são 'treinar': é esse
2 que o filtro encontra, e não três.
O que o par demonstra é que toLowerCase() dentro do filter já resolve a
comparação, sem precisar atribuir nada de volta: o filter usa o resultado da
condição, e o item original continua intacto. O descarte só é problema quando o
método é chamado para guardar o resultado — como no toUpperCase acima.
Exemplo
// Concatenar, interpolar com crase, e medir string. const nome = 'Ana'; const idade = 30; const tarefas = ['estudar', 'treinar', 'treinar']; // concatenar com + console.log('concatenar:', 'Ola, ' + nome + '!'); // interpolar com crase: o ${ } recebe expressao, nao so texto console.log('interpolar:', `Ola, ${nome}!`); console.log('expressao dentro:', `no ano que vem fara ${idade + 1} anos`); console.log('metodo dentro:', `tarefas: ${tarefas.length}`); // typeof mostra que o resultado da interpolacao e string console.log('typeof do numero:', typeof idade, '| typeof interpolado:', typeof `${idade}`); // medir console.log('length:', 'Ana'.length, '| nome completo:', `${nome} Silva`.length); console.log('string vazia:', ''.length, '| typeof:', typeof ''); // trim apaga espaco do comeco e do fim, mas nao do meio const sujo = ' Ana '; console.log('antes:', JSON.stringify(sujo), '| depois:', JSON.stringify(sujo.trim())); console.log('trimStart:', JSON.stringify(' Ana'.trimStart()), '| trimEnd:', JSON.stringify('Ana '.trimEnd())); console.log('espaco no meio continua:', JSON.stringify(' Ana Silva '.trim())); console.log('campo vazio apos trim:', ' '.trim() === ''); // caixa console.log('toUpperCase:', 'Ana'.toUpperCase(), '| toLowerCase:', 'ANA'.toLowerCase()); // string e imutavel: o metodo devolve uma string nova let usuario = 'Ana'; usuario.toUpperCase(); console.log('descartando o retorno:', usuario); usuario = usuario.toUpperCase(); console.log('guardando o retorno:', usuario); // o bug do filtro sem atribuir de volta console.log('filtrando sem atribuir:', tarefas.filter((t) => t.toLowerCase() === 'treinar').length); console.log('filtrando e guardando:', tarefas.filter((t) => t.toLowerCase() === 'treinar').length, 'item');
Saída real
concatenar: Ola, Ana! interpolar: Ola, Ana! expressao dentro: no ano que vem fara 31 anos metodo dentro: tarefas: 3 typeof do numero: number | typeof interpolado: string length: 3 | nome completo: 9 string vazia: 0 | typeof: string antes: " Ana " | depois: "Ana" trimStart: "Ana" | trimEnd: "Ana" espaco no meio continua: "Ana Silva" campo vazio apos trim: true toUpperCase: ANA | toLowerCase: ana descartando o retorno: Ana guardando o retorno: ANA filtrando sem atribuir: 2 filtrando e guardando: 2 item
Fatiar, buscar e converter
Fatiar, buscar e converter
Buscar dentro de texto e cortar texto são as duas operações que aparecem em
toda tela que mostra uma lista. includes responde a pergunta booleana,
indexOf responde onde está, slice corta, split quebra em partes.
Onde o texto está
| Chamada | Devolve | Exemplo |
|---|---|---|
texto.includes('an') | true/false | 'Ana'.includes('An') |
texto.indexOf('n') | a posição, ou -1 | 'Ana'.indexOf('n') |
texto.startsWith('An') | true/false | prefixo |
texto.endsWith('na') | true/false | sufixo |
texto.charAt(0) | o caractere da posição | 'Ana'.charAt(0) |
O -1 do indexOf é o detalhe a guardar: é ele que significa "não está". Não
existe undefined como resposta, o que torna o indexOf pouco prático para
testar e muito prático para cortar.
O exemplo usa 'cachorro' como texto e mostra as cinco chamadas da tabela em
quatro linhas. A primeira sai includes: true | indexOf: 5 — o 'rr' está na
posição 5 do texto, e o includes é a mesma pergunta já respondida. A segunda
é a do -1: nao encontrado devolve -1: -1, o z não existe em 'cachorro'.
A terceira confere as pontas, startsWith: true | endsWith: true. A quarta
compara as duas formas de pegar um caractere: charAt: c | indice direto: a —
'cachorro'[1] é a, e o charAt(1) daria a mesma coisa.
A linha seguinte aplica os mesmos dois métodos em array, e o resultado é
includes em array: true | indexOf: 1. O 1 é a posição do 'b' na lista
['a', 'b'], e não um booleano: o mesmo nome de método devolve posição nos
dois tipos, porque no array os itens também são uma sequência indexada.
includes aceita também array, com o mesmo sentido: devolve true se o elemento
está na lista.
A última linha do exemplo mede o comprimento de dois nomes e sai
length de Ana: 3 | length de José: 4. 'José' tem quatro caracteres e 'Ana'
tem três — o acento não soma unidade, porque é é um caractere só. A contagem é
por unidade de código UTF-16, e é o que o JavaScript chama de caractere: um
nome com acento vale o número de letras que se veem.
Onde a contagem engana é em caractere fora do plano básico, como emoji: eles
ocupam duas unidades e o .length devolve um número maior que o número de
símbolos desenhados. Num campo que limita tamanho de texto, o limite vale a
contagem do JavaScript, e é ela que o usuário vê preenchendo o campo.
Fatiar com slice e cortar em partes com split
slice(inicio, fim) devolve um pedaço, e o fim é exclusivo — a posição fim
não entra. Índice negativo conta de trás: slice(-3) são os três últimos.
console.log('cachorro'.slice(0, 3)); // 'cac' console.log('cachorro'.slice(-3)); // 'rro'
substring(inicio, fim) é parecido com slice, com uma diferença que já causou
bug: os argumentos invertidos são tolerados. 'cachorro'.substring(3, 1) devolve
'ac' — o substring troca os dois argumentos e fatia do menor para o maior, em
vez de devolver vazio. slice devolve string vazia nesse caso. Para código novo,
slice.
O exemplo põe os dois lado a lado numa linha só:
substring tolera os argumentos invertidos: ac | slice nao: "". O ac ao lado
do "" é a demonstração inteira — mesmo texto, mesmos argumentos, dois
comportamentos. O JSON.stringify existe ali para que a string vazia apareça
com as aspas e não desapareça da linha.
split quebra a string e devolve um array:
console.log('a, b, c'.split(', ')); // ['a', 'b', 'c'] console.log('ana.silva'.split('.')); // ['ana', 'silva']
O par split e join é o que converte entre os dois tipos:
console.log(['a', 'b', 'c'].join(', ')); // 'a, b, c'
O exemplo confirma o par nas duas direções numa linha:
split: [ 'a', 'b', 'c' ] | join: a, b, c. Do lado do split o console
desenha o array com colchetes e aspas; do lado do join, o texto solto. A
linha seguinte usa o ponto como separador, e `split por ponto: [ 'ana',
'silva' ]` mostra que o separador pode ser qualquer caractere.
O detalhe do split que aparece no app é o do separador com espaço: ele tem que
ser exatamente o mesmo que está no texto. 'a, b, c'.split(', ') acha três
partes; 'a,b,c'.split(',') também; 'a, b, c'.split(',') devolve três partes
iguais com espaço na ponta, e é aí que nasce o item ' b' que não bate com
nenhum filtro.
Substituir
replace substitui só a primeira ocorrência; replaceAll substitui todas.
Como replace não repete, a diferença entre os dois é o número de substituições,
não a forma:
console.log('a-b-c'.replace('-', '+')); // 'a+b-c' console.log('a-b-c'.replaceAll('-', '+')); // 'a+b+c'
Com dois argumentos, o segundo pode ser função, chamada uma vez por ocorrência:
console.log('a1b2'.replace(/\d/g, (d) => '#')); // 'a#b#'
A expressão regular (/\d/g) é o que substitui em todas as posições sem precisar
de replaceAll.
O exemplo mostra as três formas em três linhas, e a diferença aparece na segunda
palavra de cada uma: replace so a primeira: a+b-c deixa os dois hífens que
sobraram, replaceAll todas: a+b+c troca todos, e replace com funcao: a#b#
troca os dois dígitos. A terceira linha usa o g da expressão regular, que é o
que significa "global" — sem ele, /\d/ pararia na primeira posição.
A função no segundo argumento é chamada uma vez por ocorrência e o que ela
devolve é o texto que substitui. Com (digito) => '#' o nome do parâmetro não é
usado, porque a substituição é sempre #; um caso em que o nome importa é
dobrar o dígito, (d) => String(Number(d) * 2).
Converter texto em número
Number() converte a string inteira. Espaco nas pontas é ignorado, e texto que
não é número dá NaN — inclusive '', que dá 0.
console.log(Number(' 12 ')); // 12 console.log(Number('abc')); // NaN console.log(Number('')); // 0
parseInt para no primeiro caractere que não é número, e o segundo argumento é a
base:
console.log(parseInt('12px')); // 12 console.log(parseInt('ff', 16)); // 255
parseFloat para no ponto e devolve o número com casas decimais. String() faz o
caminho inverso e converte qualquer valor em string.
console.log(parseFloat('3.5abc')); // 3.5 console.log(String(42) + ' anos'); // '42 anos'
Em campo de formulário, a combinação que funciona é trim e depois Number, com
a checagem Number.isNaN — porque '' vira 0, e zero não é o que o campo
vazio queria dizer:
function paraNumero(texto) { const limpo = String(texto).trim(); if (limpo === '') return null; const numero = Number(limpo); return Number.isNaN(numero) ? null : numero; }
A função é o exemplo inteiro e o console a chama três vezes:
campo vazio: null | valido: 30 | invalido: null. O trim vem antes de tudo,
o campo só com espaços vira '' e sai null pelo primeiro return — antes de
qualquer conversão. ' 30 ' passa pelo trim, vira 30 no Number e sai
30. 'abc' vira NaN, é pego pelo Number.isNaN e sai null.
O null da primeira e da terceira posição é o mesmo valor e não é
undefined. A diferença importa porque o null é uma resposta explícita de
"isto não é número", e o undefined seria "não sei". O campo numérico do
TextInput devolve string sempre, mesmo vazio — e sem essa conversão, a
primeira e a terceira posição chegariam à tela como 0 e NaN.
padStart e padEnd: alinhar na tela
Quando o número vem da API e precisa de casa fixa, padStart completa pela
esquerda com um caractere até o tamanho pedido:
console.log('42'.padStart(5, '0')); // '00042' console.log('Ana'.padEnd(5, '.')); // 'Ana..'
É o método que resolve número de telefone, de documento e de horário em lista
ordenada por texto.
O exemplo fecha com padStart: 00042 | padEnd: Ana.. — cinco posições em ambos
os casos, preenchidas por lados opostos. O padStart é o que alinha coluna de
número; o padEnd alinha coluna de nome. O detalhe é que o segundo argumento é
o caractere de preenchimento e ele pode ser mais de um, ou uma string inteira
qualquer.
E o padStart só completa se faltar: '00042'.padStart(5, '0') devolve
'00042', sem mudar, porque o tamanho já foi atingido. Ele nunca corta texto
longo — para isso é o slice.
Exemplo
// Fatiar, buscar e converter texto. const texto = 'cachorro'; /* onde o texto esta */ console.log('includes:', texto.includes('rr'), '| indexOf:', texto.indexOf('rr')); console.log('nao encontrado devolve -1:', texto.indexOf('z')); console.log('startsWith:', texto.startsWith('ca'), '| endsWith:', texto.endsWith('ro')); console.log('charAt:', texto.charAt(0), '| indice direto:', texto[1]); // includes tambem funciona em array console.log('includes em array:', ['a', 'b'].includes('b'), '| indexOf:', ['a', 'b'].indexOf('b')); /* fatiar: o fim e exclusivo */ console.log('slice(0, 3):', texto.slice(0, 3), '| slice(-3):', texto.slice(-3)); console.log('substring tolera os argumentos invertidos:', texto.substring(3, 1), '| slice nao:', JSON.stringify(texto.slice(3, 1))); /* split e join: o par que converte entre os dois tipos */ console.log('split:', 'a, b, c'.split(', '), '| join:', ['a', 'b', 'c'].join(', ')); console.log('split por ponto:', 'ana.silva'.split('.')); /* substituir */ console.log('replace so a primeira:', 'a-b-c'.replace('-', '+')); console.log('replaceAll todas:', 'a-b-c'.replaceAll('-', '+')); console.log('replace com funcao:', 'a1b2'.replace(/\d/g, (digito) => '#')); /* converter */ console.log('Number:', Number(' 12 '), '| Number de texto:', Number('abc'), '| Number de vazio:', Number('')); console.log('parseInt:', parseInt('12px'), '| parseInt base 16:', parseInt('ff', 16)); console.log('parseFloat:', parseFloat('3.5abc'), '| String:', String(42) + ' anos'); /* o cuidado do campo de formulario: '' vira 0 */ function paraNumero(campo) { const limpo = String(campo).trim(); if (limpo === '') return null; const numero = Number(limpo); return Number.isNaN(numero) ? null : numero; } console.log('campo vazio:', paraNumero(' '), '| valido:', paraNumero(' 30 '), '| invalido:', paraNumero('abc')); /* padStart e padEnd alinham na tela */ console.log('padStart:', '42'.padStart(5, '0'), '| padEnd:', 'Ana'.padEnd(5, '.')); /* nome com acento e comprimento em UTF-16 */ console.log('length de Ana:', 'Ana'.length, '| length de José:', 'José'.length);
Saída real
includes: true | indexOf: 5 nao encontrado devolve -1: -1 startsWith: true | endsWith: true charAt: c | indice direto: a includes em array: true | indexOf: 1 slice(0, 3): cac | slice(-3): rro substring tolera os argumentos invertidos: ac | slice nao: "" split: [ 'a', 'b', 'c' ] | join: a, b, c split por ponto: [ 'ana', 'silva' ] replace so a primeira: a+b-c replaceAll todas: a+b+c replace com funcao: a#b# Number: 12 | Number de texto: NaN | Number de vazio: 0 parseInt: 12 | parseInt base 16: 255 parseFloat: 3.5 | String: 42 anos campo vazio: null | valido: 30 | invalido: null padStart: 00042 | padEnd: Ana.. length de Ana: 3 | length de José: 4