Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori7 min de leitura

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

js
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));
Ana Souza string São Paulo string Ana Souza — São Paulo string 1042 string

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

js
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');
O cliente disse: "quero o teclado hoje" A nota saiu no nome d'Ana A nota saiu no nome d'Ana O cliente disse: "quero o teclado hoje" Caminho da nota: C:\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

js
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);
ana souza ANA SOUZA ana Ana souza ana souza

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:

js
const sku = 'tec-001';

sku[0] = 'T';

console.log(sku);
file:///private/tmp/club-store/imutavel-erro.mjs:3 sku[0] = 'T'; ^

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

js
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'));
3 16 2 11 10 ã 1 ã 2 false true

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 usa length vai 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 de normalize('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

js
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]);
T T undefined 1 TEC 001 🛒 �

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

js
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(''));
Pedido 1042 — Ana Pedido 1042 — Ana Pedido 1042 — Ana Teclado mecânico, Mouse sem fio ab

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

js
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('-'));
" ana@CLUB.com.br " "ana@club.com.br" 00001042 TEC/001 a/b/c true [ '01310', '100' ]

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

js
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')));
false true 1 [ 'câmera', 'camiseta', 'caneca' ]

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

js
const cliente = 'Ana Souza;
const pedido = 1042;

console.log(cliente, pedido);
file:///private/tmp/club-store/aspas-nao-fechadas.mjs:1 const cliente = 'Ana Souza; ^^^^^^^^^^^

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() e normalize('NFC') nos dois lados.
  • Não conte caracteres com length se o campo aceita emoji. Use [...texto].length.
  • Ordene com localeCompare('pt-BR'), nunca com sort() 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.

  • strings
  • imutabilidade
  • utf-16
  • escapar
  • unicode

Perguntas frequentes

Se strings são imutáveis, meu código fica lento ao concatenar num laço?
Não na escala de um site. Os motores otimizam concatenação com estruturas internas que só materializam a string final quando alguém a lê. Para milhares de pedaços, junte num array e use join — mas por clareza, não por medo de performance.
Por que '🛒'.length devolve 2?
Porque length conta unidades de 16 bits, não caracteres visíveis. Emoji e vários símbolos ocupam duas unidades, chamadas de par substituto. Para contar caracteres como uma pessoa contaria, espalhe a string num array com o operador de espalhamento, que percorre pontos de código.
Aspas simples ou duplas, qual é a certa?
As duas fazem exatamente a mesma coisa. O que decide é o conteúdo e o padrão do time: se o texto tem apóstrofo, aspas duplas evitam escape; se tem aspas duplas, use simples. Ferramentas como Prettier normalizam isso sozinhas e a discussão morre.
Qual a diferença entre slice, substring e substr?
slice aceita índice negativo contando do fim e é a escolha padrão. substring troca os argumentos de lugar quando o primeiro é maior que o segundo, o que costuma esconder bug. substr está obsoleto e não deve aparecer em código novo.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — String — developer.mozilla.org
  2. MDN — String.prototype.normalize() — developer.mozilla.org
  3. ECMAScript 2026 Language Specification — String Exotic Objects — tc39.es

Continue por aqui