Strings em JavaScript: criar, concatenar e escapar
Por que toda string é imutável, como o length conta emoji e acento, e o erro de aspas não fechadas — com cada exemplo executado e a saída real no Node 24.
String é texto. Em JavaScript ela tem duas características que definem tudo o
que você pode fazer com ela: é imutável — nenhum método altera a string
original, todos devolvem uma nova — e é armazenada em unidades de 16 bits, o
que faz um emoji contar como dois no length.
Essas duas propriedades explicam a maior parte das dúvidas de iniciante:
por que nome.toUpperCase() sozinho numa linha não faz nada, e por que o contador
de caracteres do seu formulário erra quando o cliente digita um coração.
Pense numa etiqueta já impressa
Uma string se parece com uma etiqueta que saiu da impressora. Você pode ler, recortar uma parte e pedir outra em letras maiúsculas, mas não volta a tinta para trocar uma letra naquela mesma etiqueta. Cada método cria outro texto. É isso que imutável quer dizer aqui: o valor original não é alterado.
No código, nome.toUpperCase() devolve uma nova string; ele não muda nome.
Por isso você guarda o resultado em outra variável ou reatribui a existente. A
analogia da etiqueta não explica toda a memória do JavaScript, mas ajuda a fazer
a pergunta certa: “onde guardei a nova cópia?”. Antes de seguir, execute
const nome = 'Ana'; nome.toUpperCase(); console.log(nome) e tente prever a
saída. Se você esperou ANA, acabou de encontrar o ponto central da lição.
Três formas de criar
const cliente = 'Ana Souza';
const cidade = "São Paulo";
const etiqueta = `${cliente} — ${cidade}`;
console.log(cliente, typeof cliente);
console.log(cidade, typeof cidade);
console.log(etiqueta, typeof etiqueta);
console.log(String(1042), typeof String(1042));Aspas simples e aspas duplas são idênticas para o motor — não existe a distinção
que outras linguagens fazem. A crase é a template literal, com a interpolação que
a lição de template literals detalha.
String(x) é a conversão explícita, e é ela que você usa quando o valor pode não
ser texto.
Escapar: aspas dentro de aspas
console.log('O cliente disse: "quero o teclado hoje"');
console.log("A nota saiu no nome d'Ana");
console.log('A nota saiu no nome d\'Ana');
console.log("O cliente disse: \"quero o teclado hoje\"");
console.log('Caminho da nota:\tC:\\notas\\1042.pdf');Duas estratégias, e a primeira é sempre melhor: troque o delimitador em vez de escapar. Se o texto tem apóstrofo, use aspas duplas; se tem aspas duplas, use simples. A barra invertida só é obrigatória quando o texto tem os dois.
A última linha mostra os escapes que aparecem de verdade: \t é tabulação, \n
é quebra de linha, e \\ é uma barra invertida literal — necessária em caminho
do Windows e em expressão regular escrita como string.
| escape | significa | onde aparece |
|---|---|---|
\' \" |
a própria aspa | texto com citação |
\\ |
barra invertida | caminho do Windows, regex |
\n |
quebra de linha | arquivo de texto, log |
\t |
tabulação | saída alinhada no terminal |
\u00e3 |
caractere pelo código Unicode | acento vindo de sistema legado |
String é imutável: todo método devolve uma nova
const cliente = 'ana souza';
const maiuscula = cliente.toUpperCase();
const semEspaco = ' ana '.trim();
console.log(cliente);
console.log(maiuscula);
console.log(semEspaco);
console.log(cliente.replace('ana', 'Ana'));
console.log(cliente);Leia a primeira e a última linha: cliente continua 'ana souza' depois de
toUpperCase() e depois de replace(). Nenhum dos dois mexeu no original — eles
devolveram uma string nova, e quem não guarda o retorno perde o trabalho.
Esse é o bug de uma linha mais comum com strings: escrever cliente.trim();
sozinho numa linha e esperar que a variável tenha sido limpa. Ela não foi. Tem
que ser cliente = cliente.trim() — e aí a variável precisa ser let, não
const, como explica a lição de
variáveis em JavaScript.
E se você tentar mudar um caractere pelo índice, em módulo ES o motor reclama:
const sku = 'tec-001';
sku[0] = 'T';
console.log(sku);TypeError: Cannot assign to read only property ‘0’ of string ‘tec-001’ at file:///private/tmp/club-store/imutavel-erro.mjs:3:8 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v24.16.0
length conta unidades UTF-16, não caracteres
const precomposto = '\u00e3'; // ã pré-composto
const decomposto = 'a\u0303'; // a + til combinante
console.log('Ana'.length);
console.log('Teclado mecânico'.length);
console.log('🛒'.length);
console.log('Carrinho 🛒'.length);
console.log([...'Carrinho 🛒'].length);
console.log(precomposto, precomposto.length, decomposto, decomposto.length);
console.log(precomposto === decomposto);
console.log(precomposto.normalize('NFC') === decomposto.normalize('NFC'));Três resultados para guardar:
'🛒'.lengthé 2. Emoji e muitos símbolos ocupam duas unidades de 16 bits, o chamado par substituto. Um contador de caracteres que usalengthvai errar em qualquer campo onde o cliente possa digitar emoji.[...'Carrinho 🛒'].lengthé 10. O operador de espalhamento percorre pontos de código, não unidades — é a contagem que bate com o que uma pessoa vê.'ã'pode ter comprimento 1 ou 2. O mesmo caractere visível existe em duas representações: pré-composta e decomposta. Elas não são iguais no===, e ficam iguais depois denormalize('NFC'). É exatamente isso que acontece quando um nome copiado de um PDF ou vindo de um Mac não bate com o mesmo nome no banco.
Acesso por índice, at() e fatia
const sku = 'TEC-001';
console.log(sku[0]);
console.log(sku.charAt(0));
console.log(sku[99]);
console.log(sku.at(-1));
console.log(sku.slice(0, 3));
console.log(sku.slice(-3));
console.log([...'🛒Club'][0]);
console.log('🛒Club'[0]);sku[99] devolve undefined em vez de lançar erro — o que significa que um
índice fora do intervalo só vai estourar na linha seguinte, quando alguém chamar
um método nesse undefined. at(-1) pega o último caractere sem fazer conta com
length.
As duas últimas linhas fecham o assunto do UTF-16: espalhando a string, o
primeiro item é o carrinho inteiro; acessando por índice, você recebe meio
emoji — uma unidade solta que o terminal desenha como o losango de
substituição. Para texto com emoji, use o espalhamento ou
Intl.Segmenter.
Concatenar: +, concat e join
const cliente = 'Ana';
const pedido = 1042;
const itens = ['Teclado mecânico', 'Mouse sem fio'];
console.log('Pedido ' + pedido + ' — ' + cliente);
console.log('Pedido '.concat(pedido, ' — ', cliente));
console.log(`Pedido ${pedido} — ${cliente}`);
console.log(itens.join(', '));
console.log(['a', 'b'].join(''));As três primeiras produzem a mesma string. A terceira é a que você deve escrever.
concat existe, quase ninguém usa, e não tem vantagem sobre as outras duas.
Para juntar uma lista, join é sempre melhor que somar num laço: ele diz a
intenção e escolhe o separador num lugar só. Vale lembrar que o + com número
concatena em vez de somar quando um dos lados é string — o comportamento
detalhado em
operadores aritméticos em JavaScript.
Os métodos que você vai usar toda semana
const digitado = ' ana@CLUB.com.br ';
const email = digitado.trim().toLowerCase();
console.log(JSON.stringify(digitado));
console.log(JSON.stringify(email));
console.log(String(1042).padStart(8, '0'));
console.log('TEC-001'.replace('-', '/'));
console.log('a-b-c'.replaceAll('-', '/'));
console.log('Teclado mecânico'.includes('mec'));
console.log('01310-100'.split('-'));O JSON.stringify está aí só para as aspas mostrarem onde o espaço começa e
termina. Três detalhes que economizam tempo: replace troca só a primeira
ocorrência (por isso existe replaceAll); padStart é como se formata número de
pedido com zeros à esquerda; e split devolve array, encaixando direto no join
do exemplo anterior.
Como todos esses métodos devolvem string nova, eles encadeiam — trim() seguido
de toLowerCase() na mesma linha. Esse encadeamento é possível justamente por
causa da imutabilidade.
Comparar texto digitado com texto salvo
const digitado = 'ANA souza ';
const salvo = 'Ana Souza';
console.log(digitado === salvo);
console.log(digitado.trim().toLowerCase() === salvo.toLowerCase());
console.log('camiseta'.localeCompare('câmera', 'pt-BR'));
console.log(['camiseta', 'câmera', 'caneca'].sort((a, b) => a.localeCompare(b, 'pt-BR')));Comparação de string é sensível a maiúscula e a espaço nas pontas — por isso
trim().toLowerCase() nos dois lados antes de comparar login, e-mail ou cupom.
Para ordenar, localeCompare com pt-BR é obrigatório: o sort() padrão
compara códigos Unicode e joga toda palavra acentuada para o fim do catálogo.
Erro comum: aspas não fechadas
const cliente = 'Ana Souza;
const pedido = 1042;
console.log(cliente, pedido);SyntaxError: Invalid or unexpected token at compileSourceTextModule (node:internal/modules/esm/utils:318:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:90:18) at #translate (node:internal/modules/esm/loader:435:20) at afterLoad (node:internal/modules/esm/loader:491:29) at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:496:12) at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:549:36) at afterResolve (node:internal/modules/esm/loader:597:52) at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:603:12) at node:internal/modules/esm/loader:622:32 at TracingChannel.tracePromise (node:diagnostics_channel:362:14)
Node.js v24.16.0
A mensagem é vaga de propósito: o motor achou uma aspa aberta, correu até o fim da linha e não encontrou o par. Como string com aspas não pode atravessar linhas, ele desiste ali.
Três coisas que ajudam a achar rápido:
- O acento circunflexo aponta o começo, não o fim. O erro está em algum lugar a partir dali — quase sempre uma aspa faltando ou uma aspa errada fechando.
- A linha citada pode não ser a culpada. Se você abriu a aspa duas linhas antes, o motor só percebe onde a leitura quebra.
- Confie no realce do editor. Quando o resto do arquivo inteiro fica colorido como string, a aspa não fechada está no ponto em que a cor mudou.
O primo desse erro é SyntaxError: Unexpected identifier, que aparece quando
você esquece o + entre dois pedaços de texto — outro motivo para preferir a
crase, como mostra a lição de
template literals.
Boas práticas
- Não altere, reatribua.
nome = nome.trim(). Método de string sem atribuição é linha morta. - Normalize antes de comparar.
trim(),toLowerCase()enormalize('NFC')nos dois lados. - Não conte caracteres com
lengthse o campo aceita emoji. Use[...texto].length. - Ordene com
localeCompare('pt-BR'), nunca comsort()puro. - Prefira crase quando houver qualquer variável no meio do texto.
O próximo passo da trilha de JavaScript é usar esse
texto para tomar decisões: comparar, normalizar e ramificar o programa com
if e else — onde o trim().toLowerCase() desta lição
vira condição. Se quiser rever os fundamentos que sustentam esta lição, o
guia de JavaScript mostra a ordem completa, e a página
da tecnologia JavaScript reúne tudo o que já foi
publicado sobre a linguagem.
Perguntas frequentes
Se strings são imutáveis, meu código fica lento ao concatenar num laço?
Por que '🛒'.length devolve 2?
Aspas simples ou duplas, qual é a certa?
Qual a diferença entre slice, substring e substr?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — String — developer.mozilla.org
- MDN — String.prototype.normalize() — developer.mozilla.org
- ECMAScript 2026 Language Specification — String Exotic Objects — tc39.es


