Saltar al contenido principal
JavaScript (ECMAScript 2024) Estándar: Unicode 17.0 / ECMA-262 Time: 8 min de lectura Revisado: septiembre de 2024

Cadenas Unicode en JavaScript

Domine Unicode en JavaScript: pares sustitutos UTF-16, String.prototype.codePointAt, secuencias de escape de ES2024 e Intl.Segmenter.

TL;DR — Respuesta directa

Respuesta técnica directa

Las cadenas de JavaScript son secuencias de unidades de código UTF-16 de 16 bits, no caracteres Unicode individuales ni símbolos percibidos por el usuario. Los caracteres suplementarios fuera del Basic Multilingual Plane (como los emojis y los ideogramas poco frecuentes) requieren dos unidades de código UTF-16 (un par sustituto), mientras que los grafemas compuestos requieren varios puntos de código.

💡
Regla de producción: Nunca use string.length ni string[i] para medir o trocear texto visible para el usuario. Use String.prototype.codePointAt() para los puntos de código completos e Intl.Segmenter para los clústeres de grafemas percibidos por el usuario.

La diferencia entre unidades de código UTF-16, puntos de código Unicode y clústeres de grafemas en JavaScript

JAVASCRIPT unicode-distinction.js
const emoji = '😀';

// 1. String.prototype.length counts UTF-16 code units
console.log(emoji.length); // 2

// 2. String iterator ([...str]) counts Unicode code points
console.log([...emoji].length); // 1

// 3. Composite emoji with Zero-Width Joiner (ZWJ)
const family = '👨‍👦';
console.log(family.length); // 5 code units
console.log([...family].length); // 3 code points

// 4. Intl.Segmenter counts user-perceived grapheme clusters
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
console.log([...segmenter.segment(family)].length); // 1 user character
Salida del programa
2
1
5
3
1

Por qué ocurre esto: el modelo de cadenas de ECMAScript

Para entender el comportamiento de las cadenas de JavaScript, los desarrolladores deben comprender las tres capas distintas de representación Unicode en el navegador y en Node.js.

1. El legado histórico de UTF-16

Cuando Brendan Eich diseñó JavaScript en 1995, Unicode estaba definido como una codificación de anchura fija de 16 bits (UCS-2) con un máximo de 65.536 caracteres (el Basic Multilingual Plane, BMP). En 1996, Unicode se amplió a 1.114.112 puntos de código (planos 0 a 16). En lugar de romper las API existentes, ECMAScript adoptó UTF-16. En UTF-16, los caracteres por encima de U+FFFF se representan como dos unidades de código de 16 bits llamadas par sustituto: un sustituto alto (\uD800..\uDBFF) seguido de un sustituto bajo (\uDC00..\uDFFF).

2. Unidades de código frente a puntos de código

Un punto de código es el valor numérico abstracto (U+0000..U+10FFFF). El emoji de cara sonriente 😀 es el punto de código U+1F600. En la memoria de JavaScript, U+1F600 se codifica como las dos unidades de código UTF-16 0xD83D y 0xDE00. Métodos como .charCodeAt(0) y la propiedad .length inspeccionan solo unidades individuales de 16 bits y ven dos unidades en lugar de un carácter.

3. Puntos de código frente a clústeres de grafemas percibidos por el usuario

Incluso iterar por punto de código mediante Array.from() o for...of es insuficiente para el texto moderno. Las secuencias de emoji compuestos (como 👨‍👦) constan de varios puntos de código: hombre (U+1F468) + ZWJ (U+200D) + niño (U+1F466). Del mismo modo, las marcas combinantes como «e» (U+0065) + acento agudo combinante (U+0301) forman dos puntos de código que se renderizan como un glifo visible «é». Solo Intl.Segmenter, conforme a Unicode UAX #29, mide con precisión los caracteres visuales.

4. Sustitutos huérfanos y cadenas mal formadas

Como las cadenas de JavaScript son secuencias de enteros arbitrarios de 16 bits, pueden contener sustitutos huérfanos (p. ej., "\uD83D" sin un sustituto bajo acompañante). ECMAScript permite sustitutos huérfanos en memoria, pero las API que requieren UTF-8 bien formado (como TextEncoder, fetch() o WebSockets) reemplazarán los sustitutos huérfanos por U+FFFD (Replacement Character) o lanzarán un error.

Modelo de ejecución del lenguaje y del entorno

Lenguaje de destino JavaScript
Estándar moderno ECMAScript 2024
Nivel mínimo ES2015 (ES6)
Compatibilidad con segmentadores Chrome 87+, Safari 14.1+, Firefox 125+, Node.js 16.0+, Bun 1.0+, Deno 1.8+
Optimización del motor

V8 utiliza una optimización de representación interna: las cadenas que contienen solo caracteres Latin-1 (U+0000..U+00FF) se almacenan como matrices de 1 byte (SeqOneByteString). Las cadenas que contienen cualquier carácter por encima de U+00FF se promocionan a matrices UTF-16 de 2 bytes (SeqTwoByteString).

Errores comunes frente a patrones de producción

Aprenda qué patrones fallan en producción y cuáles son las alternativas modernas conformes con los estándares.

Contar caracteres visibles para el usuario

Contador de caracteres en formularios de interfaz, límites de SMS, campos de biografía.
EVITAR: ❌ String.prototype.length
const str = '👨‍💻';
console.log(str.length);
// Returns 5 (UTF-16 code units)
Por qué falla: String.length cuenta unidades de código de 16 bits. Los emojis con joiners de anchura cero y los pares sustitutos informan recuentos inflados.
RECOMENDADO: ✅ Intl.Segmenter (UAX #29)
const str = '👨‍💻';
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
console.log([...segmenter.segment(str)].length);
// Returns 1 (true user-perceived character)
Por qué es mejor: Implementa los límites de clústeres de grafemas extendidos de Unicode UAX #29, vinculando correctamente marcas combinantes, modificadores y secuencias ZWJ.

Iterar los caracteres de una cadena

Analizar tokens, construir transformadores de cadenas personalizados.
EVITAR: ❌ Bucle por índice o str.split("")
const text = '🔥hello';
for (let i = 0; i < text.length; i++) {
  console.log(text[i]); // splits 🔥 into \uD83D and \uDD25!
}
const chars = text.split(''); // ['\uD83D', '\uDD25', 'h', 'e', 'l', 'l', 'o']
Por qué falla: El acceso directo por corchetes text[i] y split("") operan sobre unidades de código, partiendo los pares sustitutos por la mitad y produciendo sustitutos huérfanos no válidos.
RECOMENDADO: ✅ for...of o Array.from()
const text = '🔥hello';
for (const char of text) {
  console.log(char); // '🔥', 'h', 'e', 'l', 'l', 'o'
}
const chars = [...text]; // ['🔥', 'h', 'e', 'l', 'l', 'o']
Por qué es mejor: El iterador de cadenas de ECMAScript es consciente de los puntos de código y avanza automáticamente por los pares sustitutos alto/bajo de forma conjunta.

Leer puntos de código numéricos

Convertidores de codificación de caracteres, hash criptográfico, métricas de fuentes.
EVITAR: ❌ charCodeAt(i)
const char = '😀';
console.log(char.charCodeAt(0).toString(16));
// Returns 'd83d' (High surrogate only!)
Por qué falla: charCodeAt devuelve únicamente el número de 16 bits en ese desplazamiento. Para los caracteres suplementarios, ofrece solo la mitad del par sustituto.
RECOMENDADO: ✅ codePointAt(i)
const char = '😀';
console.log(char.codePointAt(0).toString(16));
// Returns '1f600' (True Unicode code point!)
Por qué es mejor: codePointAt inspecciona la unidad de código actual y la siguiente; si forman un par sustituto válido, devuelve el valor escalar de 21 bits.

Escribir secuencias de escape Unicode

Literales de código fuente, payloads JSON, definiciones de expresiones regulares.
EVITAR: ❌ Escapes heredados de pares sustitutos
const fire = '\uD83D\uDD25'; // 🔥
// Prone to transposition, hard to read, manual math needed
Por qué falla: Obliga a los desarrolladores a calcular manualmente los desplazamientos hexadecimales de los sustitutos alto y bajo.
RECOMENDADO: ✅ Escapes Unicode con llaves de ES6
const fire = '\u{1F525}'; // 🔥
const smile = '\u{1F600}'; // 😀
Por qué es mejor: Acepta directamente cualquier punto de código Unicode válido de \u{0} a \u{10FFFF} sin cálculo de sustitutos.

Matriz de casos límite e invariantes

Una matriz de pruebas diversa entre caracteres que compara el comportamiento de los caracteres en ASCII, marcas combinantes, emojis, banderas y fragmentos sustitutos.

Caso de prueba Glifo de entrada Unidades de código Puntos de código Grafemas Categoría Explicación técnica
Letra latina ASCII A 1 1 1 Basic Latin U+0041 cabe en un solo byte de 7 bits, una unidad de código de 16 bits y un punto de código.
Acento precompuesto é 1 1 1 Latin-1 Supplement U+00E9 acento agudo precompuesto en el BMP. 1 unidad de código, 1 punto de código, 1 grafema.
Acento descompuesto 2 2 1 Combining Mark Letra base e (U+0065) + acento agudo combinante (U+0301). 2 puntos de código se renderizan como 1 grafema visual.
Símbolo de moneda 1 1 1 Currency Signo del euro U+20AC ubicado en el BMP. 1 unidad de código, 1 punto de código, 1 grafema.
Emoji estándar 😀 2 1 1 Supplementary Plane Cara sonriente U+1F600 en el plano 1. Codificado como 2 unidades de código UTF-16 (D83D DE00).
Secuencia de emoji con ZWJ 👨‍👦 5 3 1 Emoji Sequence Hombre (U+1F468) + ZWJ (U+200D) + niño (U+1F466). 3 puntos de código unidos como 1 glifo de familia.
Bandera del arcoíris 🏳️‍🌈 6 4 1 Emoji Sequence Bandera blanca (U+1F3F3) + selector de variación 16 (U+FE0F) + ZWJ (U+200D) + arcoíris (U+1F308).
Bandera nacional (indicadores regionales) 🇺🇳 4 2 1 Flag Sequence Indicador regional U (U+1F1FA) + indicador regional N (U+1F1F3). 2 puntos de código, 1 grafema.
Sustituto alto huérfano 1 1 1 Surrogate Fragment Sustituto alto aislado 0xD83D. Válido en la memoria de ECMAScript, pero mal formado en UTF-8.

Core API Reference

Standard library methods, runtime signatures, and Unicode semantics.

str.codePointAt(index: number): number | undefined

Devuelve el valor del punto de código Unicode (entero) en el índice de unidad de código UTF-16 especificado.

Semántica de Unicode: Si el índice apunta al inicio de un par sustituto válido, devuelve el punto de código completo de 21 bits (U+0000..U+10FFFF). Si apunta a un sustituto bajo o a un sustituto solitario, devuelve solo el valor de la unidad de código sustituta.
⚠️ Advertencias importantes: El parámetro índice sigue siendo un desplazamiento en unidades de código UTF-16, no en puntos de código. Para avanzar carácter a carácter, avance de 2 en 2 si codePoint >= 0x10000.
String.fromCodePoint(...codePoints: number[]): string

Crea una cadena a partir de una secuencia de puntos de código Unicode de 21 bits.

Semántica de Unicode: Calcula y emite automáticamente los pares sustitutos alto y bajo para los puntos de código >= 0x10000.
⚠️ Advertencias importantes: Lanza un RangeError si un punto de código es negativo, mayor que 0x10FFFF o NaN.
new Intl.Segmenter(locales?: string | string[], options?: { granularity: "grapheme" | "word" | "sentence" })

Proporciona una segmentación de texto consciente de la configuración regional, lo que permite una iteración fiable de los clústeres de grafemas percibidos por el usuario.

Semántica de Unicode: Se ajusta a los algoritmos predeterminados de segmentación de texto del Unicode Standard Annex #29 (UAX #29).
⚠️ Advertencias importantes: Disponible en Chrome 87+, Safari 14.1+, Firefox 125+, Node.js 16.0+. Use un polyfill para navegadores antiguos.
str.normalize(form?: "NFC" | "NFD" | "NFKC" | "NFKD"): string

Devuelve la forma de normalización Unicode de la cadena que realiza la llamada.

Semántica de Unicode: Se ajusta al Unicode Standard Annex #15 (UAX #15). NFC (predeterminado): descomposición canónica seguida de composición canónica.
⚠️ Advertencias importantes: Normalice siempre la entrada del usuario a NFC antes de comparar nombres de usuario o almacenar texto en bases de datos.
new RegExp(pattern, "v")

Indicador UnicodeSets de expresiones regulares de ECMAScript 2024 que habilita operaciones de conjuntos y propiedades de cadena.

Semántica de Unicode: Sustituye al indicador /u. Admite \p{Extended_Pictographic} y conjuntos de cadenas de varios caracteres como [\p{White_Space}--[\n\r]].
⚠️ Advertencias importantes: Compatible con Chrome 112+, Safari 17+, Firefox 116+, Node.js 20+.
Herramienta interactiva

Inspector interactivo de cadenas y Unicode en JavaScript

Pruebe cualquier cadena o secuencia de emojis en tiempo real para inspeccionar sus unidades de código UTF-16, puntos de código Unicode y clústeres de grafemas en su navegador.

Unidades de código UTF-16 (str.length) 11 palabras de almacenamiento de 16 bits
Puntos de código Unicode ([...str]) 7 Caracteres individuales
Clústeres de grafemas (Intl.Segmenter) 1 Glifos percibidos por el usuario
UTF-8 Bytes 25 Octetos serializados

Desglose de puntos de código:

Procedencia de estándares y fuentes

Todos los invariantes técnicos, las API y los comportamientos de esta guía se verifican contra las especificaciones primarias oficiales.

Ecma International

ECMAScript 2024 Language Specification (ECMA-262 15th Edition)

Cláusula: §6.1.4 The String Type & §22.1 String Objects

Ecma International

ECMAScript Internationalization API (ECMA-402)

Cláusula: §18 Intl.Segmenter Objects

Unicode Consortium

Unicode Standard Annex #29: Unicode Text Segmentation

Cláusula: §3 Grapheme Cluster Boundaries

Unicode Consortium

Unicode Standard Annex #15: Unicode Normalization Forms

Cláusula: §1 Normalization Forms

MDN Web Docs

JavaScript String Reference & UTF-16 Representation

Cláusula: UTF-16 Characters, Unicode Code Points, and Grapheme Clusters