Представь: пользователь вводит имя «José» или никнейм с эмодзи «Александр 🎸». Твоя функция обрезки до 20 символов работает неправильно — режет эмодзи пополам, получается кракозябра. Или счётчик символов в твиттере показывает 4 вместо 2 для «😀🎉». Это последствия работы с str.length вместо реального количества символов.
JavaScript строки хранятся в UTF-16, где некоторые символы занимают две «кодовые единицы». str.length считает единицы, а не видимые символы. Понимание этого позволяет корректно работать со строками, содержащими эмодзи, редкие иероглифы или диакритику.
slice, length, split работают с кодовыми единицами, а не символамиu в регулярных выражениях включает Unicode-режимЮникод содержит более 1 миллиона символов. Базовая многоязычная плоскость (BMP) включает символы с кодовыми точками от U+0000 до U+FFFF — они занимают одну кодовую единицу UTF-16.
Символы за пределами BMP (U+10000 и выше) — иероглифы, эмодзи, математические символы — кодируются парой кодовых единиц (суррогатная пара):
const latin = 'A' // U+0041 — одна кодовая единица
const emoji = '😀' // U+1F600 — СУРРОГАТНАЯ ПАРА (две единицы!)
const kanji = '𠮷' // U+20BB7 — редкий иероглиф, суррогатная пара
console.log(latin.length) // 1 — корректно
console.log(emoji.length) // 2 — НЕ 1! Это ловушка
console.log(kanji.length) // 2
// charCodeAt возвращает кодовые ЕДИНИЦЫ (не кодовые точки!)
console.log(emoji.charCodeAt(0)) // 55357 (первая половина суррогатной пары)
console.log(emoji.charCodeAt(1)) // 56832 (вторая половина)const emoji = '😀'
// charCodeAt — возвращает кодовую ЕДИНИЦУ (только 0..65535)
console.log(emoji.charCodeAt(0)) // 55357 — половина суррогатной пары
// codePointAt — возвращает кодовую ТОЧКУ (может быть > 65535)
console.log(emoji.codePointAt(0)) // 128512 (0x1F600) — настоящее значение
// String.fromCharCode vs String.fromCodePoint
console.log(String.fromCharCode(65)) // 'A'
console.log(String.fromCodePoint(128512)) // '😀' — работает с любыми кодовыми точками
console.log(String.fromCharCode(128512)) // мусор — обрезает до 16 битconst str = 'Hello 😀 мир 𠮷'
console.log(str.length) // больше, чем количество «видимых» символов!
// Посчитать символы правильно:
const chars = [...str] // итератор строки понимает суррогатные пары
console.log(chars.length) // реальное количество символов
// Array.from работает аналогично
console.log(Array.from(str).length) // то же самоеОператор spread ([...str]) и Array.from используют итератор строки, который понимает суррогатные пары и возвращает полные символы.
const text = 'Привет 🌍 мир!'
// НЕПРАВИЛЬНО — может разрезать суррогатную пару
const badSlice = text.slice(0, 9) // может оборвать эмодзи
// ПРАВИЛЬНО — через spread
const chars = [...text]
const goodSlice = chars.slice(0, 9).join('')
// Разворот строки
const reversed = [...text].reverse().join('') // правильно
const badReversed = text.split('').reverse().join('') // ЛОМАЕТ эмодзи!
// Итерация по символам
for (const char of text) {
// Корректно — for...of использует итератор
console.log(char)
}Один и тот же видимый символ может быть закодирован несколькими способами:
// 'é' можно записать двумя способами:
const combined = '\u00E9' // один символ: é (NFC)
const decomposed = 'e\u0301' // два символа: e + акцент (NFD)
console.log(combined === decomposed) // false — строки разные!
console.log(combined.length) // 1
console.log(decomposed.length) // 2
// normalize() приводит к одной форме
console.log(combined.normalize('NFC') === decomposed.normalize('NFC')) // trueФормы нормализации:
// Простое сравнение через < > не учитывает локаль
const words = ['ёж', 'еда', 'яблоко', 'абрикос']
console.log(words.sort()) // неправильный порядок!
// Правильно
console.log(words.sort((a, b) => a.localeCompare(b, 'ru')))
// ['абрикос', 'еда', 'ёж', 'яблоко']
// С акцентами
const names = ['résumé', 'resume', 'Résumé']
console.log(names.sort((a, b) => a.localeCompare(b, 'fr', { sensitivity: 'base' })))function countChars(str) {
return [...str].length
}
function truncate(str, maxChars, suffix = '…') {
const chars = [...str]
if (chars.length <= maxChars) return str
return chars.slice(0, maxChars).join('') + suffix
}
const post = 'Привет 🌍! Сегодня 🌞 хорошая погода 🌈'
console.log('length (UTF-16):', post.length) // больше реального
console.log('countChars:', countChars(post)) // реальное число символов
console.log(truncate(post, 10)) // 'Привет 🌍! С…'
console.log(truncate(post, 100)) // вся строка без изменений1. Разворачивать строку через split('').reverse() — ломает эмодзи
const str = 'Hi 😀!'
// ПЛОХО — split('') делит по кодовым единицам, эмодзи разбивается
const bad = str.split('').reverse().join('')
console.log(bad) // '!\uDC00\uD83D iH' — сломанный эмодзи
// ХОРОШО — spread использует итератор, понимающий суррогатные пары
const good = [...str].reverse().join('')
console.log(good) // '!😀 iH' — корректно2. Сравнивать строки с диакритикой через ===
// ПЛОХО — визуально одинаковые строки могут быть разными
const a = 'é' // U+00E9 (один символ, NFC)
const b = 'é' // e + комбинирующий акцент (два кодовых блока, NFD)
console.log(a === b) // false — строки разные!
console.log(a.length) // 1
console.log(b.length) // 2
// ХОРОШО — нормализуй перед сравнением
console.log(a.normalize('NFC') === b.normalize('NFC')) // true3. Сортировать строки с кириллицей через sort() без localeCompare
// ПЛОХО — sort() использует кодовые точки, ё идёт не там
const words = ['яблоко', 'ёж', 'абрикос', 'дерево']
console.log(words.sort()) // ['абрикос', 'дерево', 'яблоко', 'ёж'] — ёж в конце!
// ХОРОШО — localeCompare знает порядок алфавита
console.log(words.sort((a, b) => a.localeCompare(b, 'ru')))
// ['абрикос', 'дерево', 'ёж', 'яблоко']Intl.Segmenter или [...str].length для корректного подсчётаlocaleCompare для сортировки имён пользователейСуррогатные пары: length vs реальное количество символов, codePointAt, spread-итерация
// Демонстрация суррогатных пар и правильной работы со строками
// 1. Разница между .length и реальным числом символов
console.log('=== length vs реальное количество ===')
const examples = [
{ label: 'ASCII', str: 'Hello' },
{ label: 'Кириллица', str: 'Привет' },
{ label: 'Эмодзи', str: '😀🎉🌍' },
{ label: 'Смешанная', str: 'Hi 😀 мир 🌍!' },
{ label: 'Иероглифы', str: '日本語' },
]
for (const { label, str } of examples) {
const realCount = [...str].length
console.log(`${label.padEnd(12)}: length=${str.length}, символов=${realCount}${str.length !== realCount ? ' ⚠️ РАЗНИЦА' : ''}`)
}
// 2. codePointAt vs charCodeAt
console.log('\n=== codePointAt vs charCodeAt ===')
const emoji = '😀'
console.log('charCodeAt(0):', emoji.charCodeAt(0)) // 55357 (суррогат)
console.log('charCodeAt(1):', emoji.charCodeAt(1)) // 56832 (суррогат)
console.log('codePointAt(0):', emoji.codePointAt(0)) // 128512 (0x1F600 — настоящая кодовая точка)
// Восстановить символ из кодовой точки
const cp = emoji.codePointAt(0)
console.log('fromCodePoint:', String.fromCodePoint(cp)) // 😀
// 3. Итерация: for...of корректно обрабатывает суррогатные пары
console.log('\n=== Итерация по символам ===')
const mixed = 'A😀B🌍C'
console.log('Через for...of (правильно):')
const fromFor = []
for (const ch of mixed) {
fromFor.push(ch)
}
console.log(fromFor) // ['A', '😀', 'B', '🌍', 'C'] — 5 символов
console.log('Через индекс (неправильно):')
const fromIndex = []
for (let i = 0; i < mixed.length; i++) {
fromIndex.push(mixed[i])
}
console.log(fromIndex) // 7 элементов — суррогатные пары разбиты
// 4. Разворот строки
console.log('\n=== Разворот строки ===')
const str = 'Привет 🌍'
const badReverse = str.split('').reverse().join('')
const goodReverse = [...str].reverse().join('')
console.log('Оригинал:', str)
console.log('Плохой разворот:', badReverse) // 🌍 ломается
console.log('Правильный разворот:', goodReverse) // корректно
// 5. Нормализация Unicode
console.log('\n=== Нормализация Unicode ===')
const nfc = '\u00E9' // é — один символ (NFC)
const nfd = 'e\u0301' // e + комбинирующий акцент (NFD)
console.log('NFC:', nfc, '| length:', nfc.length) // 1
console.log('NFD:', nfd, '| length:', nfd.length) // 2
console.log('Равны без нормализации:', nfc === nfd) // false
console.log('Равны после normalize:', nfc.normalize('NFC') === nfd.normalize('NFC')) // true
// 6. Корректный подсчёт и усечение
console.log('\n=== countChars и truncate ===')
function countChars(str) {
return [...str].length
}
function truncate(str, maxChars, suffix = '…') {
const chars = [...str]
if (chars.length <= maxChars) return str
return chars.slice(0, maxChars).join('') + suffix
}
const post = 'Добро пожаловать 🎉 в наш чат! Здесь весело 😄🌟'
console.log('UTF-16 units:', post.length)
console.log('Реальных символов:', countChars(post))
console.log('\nОбрезаем до 20 символов:')
console.log(truncate(post, 20))
console.log('Обрезаем до 35 символов:')
console.log(truncate(post, 35))
console.log('Обрезаем до 200 (без изменений):')
console.log(truncate(post, 200))Представь: пользователь вводит имя «José» или никнейм с эмодзи «Александр 🎸». Твоя функция обрезки до 20 символов работает неправильно — режет эмодзи пополам, получается кракозябра. Или счётчик символов в твиттере показывает 4 вместо 2 для «😀🎉». Это последствия работы с str.length вместо реального количества символов.
JavaScript строки хранятся в UTF-16, где некоторые символы занимают две «кодовые единицы». str.length считает единицы, а не видимые символы. Понимание этого позволяет корректно работать со строками, содержащими эмодзи, редкие иероглифы или диакритику.
slice, length, split работают с кодовыми единицами, а не символамиu в регулярных выражениях включает Unicode-режимЮникод содержит более 1 миллиона символов. Базовая многоязычная плоскость (BMP) включает символы с кодовыми точками от U+0000 до U+FFFF — они занимают одну кодовую единицу UTF-16.
Символы за пределами BMP (U+10000 и выше) — иероглифы, эмодзи, математические символы — кодируются парой кодовых единиц (суррогатная пара):
const latin = 'A' // U+0041 — одна кодовая единица
const emoji = '😀' // U+1F600 — СУРРОГАТНАЯ ПАРА (две единицы!)
const kanji = '𠮷' // U+20BB7 — редкий иероглиф, суррогатная пара
console.log(latin.length) // 1 — корректно
console.log(emoji.length) // 2 — НЕ 1! Это ловушка
console.log(kanji.length) // 2
// charCodeAt возвращает кодовые ЕДИНИЦЫ (не кодовые точки!)
console.log(emoji.charCodeAt(0)) // 55357 (первая половина суррогатной пары)
console.log(emoji.charCodeAt(1)) // 56832 (вторая половина)const emoji = '😀'
// charCodeAt — возвращает кодовую ЕДИНИЦУ (только 0..65535)
console.log(emoji.charCodeAt(0)) // 55357 — половина суррогатной пары
// codePointAt — возвращает кодовую ТОЧКУ (может быть > 65535)
console.log(emoji.codePointAt(0)) // 128512 (0x1F600) — настоящее значение
// String.fromCharCode vs String.fromCodePoint
console.log(String.fromCharCode(65)) // 'A'
console.log(String.fromCodePoint(128512)) // '😀' — работает с любыми кодовыми точками
console.log(String.fromCharCode(128512)) // мусор — обрезает до 16 битconst str = 'Hello 😀 мир 𠮷'
console.log(str.length) // больше, чем количество «видимых» символов!
// Посчитать символы правильно:
const chars = [...str] // итератор строки понимает суррогатные пары
console.log(chars.length) // реальное количество символов
// Array.from работает аналогично
console.log(Array.from(str).length) // то же самоеОператор spread ([...str]) и Array.from используют итератор строки, который понимает суррогатные пары и возвращает полные символы.
const text = 'Привет 🌍 мир!'
// НЕПРАВИЛЬНО — может разрезать суррогатную пару
const badSlice = text.slice(0, 9) // может оборвать эмодзи
// ПРАВИЛЬНО — через spread
const chars = [...text]
const goodSlice = chars.slice(0, 9).join('')
// Разворот строки
const reversed = [...text].reverse().join('') // правильно
const badReversed = text.split('').reverse().join('') // ЛОМАЕТ эмодзи!
// Итерация по символам
for (const char of text) {
// Корректно — for...of использует итератор
console.log(char)
}Один и тот же видимый символ может быть закодирован несколькими способами:
// 'é' можно записать двумя способами:
const combined = '\u00E9' // один символ: é (NFC)
const decomposed = 'e\u0301' // два символа: e + акцент (NFD)
console.log(combined === decomposed) // false — строки разные!
console.log(combined.length) // 1
console.log(decomposed.length) // 2
// normalize() приводит к одной форме
console.log(combined.normalize('NFC') === decomposed.normalize('NFC')) // trueФормы нормализации:
// Простое сравнение через < > не учитывает локаль
const words = ['ёж', 'еда', 'яблоко', 'абрикос']
console.log(words.sort()) // неправильный порядок!
// Правильно
console.log(words.sort((a, b) => a.localeCompare(b, 'ru')))
// ['абрикос', 'еда', 'ёж', 'яблоко']
// С акцентами
const names = ['résumé', 'resume', 'Résumé']
console.log(names.sort((a, b) => a.localeCompare(b, 'fr', { sensitivity: 'base' })))function countChars(str) {
return [...str].length
}
function truncate(str, maxChars, suffix = '…') {
const chars = [...str]
if (chars.length <= maxChars) return str
return chars.slice(0, maxChars).join('') + suffix
}
const post = 'Привет 🌍! Сегодня 🌞 хорошая погода 🌈'
console.log('length (UTF-16):', post.length) // больше реального
console.log('countChars:', countChars(post)) // реальное число символов
console.log(truncate(post, 10)) // 'Привет 🌍! С…'
console.log(truncate(post, 100)) // вся строка без изменений1. Разворачивать строку через split('').reverse() — ломает эмодзи
const str = 'Hi 😀!'
// ПЛОХО — split('') делит по кодовым единицам, эмодзи разбивается
const bad = str.split('').reverse().join('')
console.log(bad) // '!\uDC00\uD83D iH' — сломанный эмодзи
// ХОРОШО — spread использует итератор, понимающий суррогатные пары
const good = [...str].reverse().join('')
console.log(good) // '!😀 iH' — корректно2. Сравнивать строки с диакритикой через ===
// ПЛОХО — визуально одинаковые строки могут быть разными
const a = 'é' // U+00E9 (один символ, NFC)
const b = 'é' // e + комбинирующий акцент (два кодовых блока, NFD)
console.log(a === b) // false — строки разные!
console.log(a.length) // 1
console.log(b.length) // 2
// ХОРОШО — нормализуй перед сравнением
console.log(a.normalize('NFC') === b.normalize('NFC')) // true3. Сортировать строки с кириллицей через sort() без localeCompare
// ПЛОХО — sort() использует кодовые точки, ё идёт не там
const words = ['яблоко', 'ёж', 'абрикос', 'дерево']
console.log(words.sort()) // ['абрикос', 'дерево', 'яблоко', 'ёж'] — ёж в конце!
// ХОРОШО — localeCompare знает порядок алфавита
console.log(words.sort((a, b) => a.localeCompare(b, 'ru')))
// ['абрикос', 'дерево', 'ёж', 'яблоко']Intl.Segmenter или [...str].length для корректного подсчётаlocaleCompare для сортировки имён пользователейСуррогатные пары: length vs реальное количество символов, codePointAt, spread-итерация
// Демонстрация суррогатных пар и правильной работы со строками
// 1. Разница между .length и реальным числом символов
console.log('=== length vs реальное количество ===')
const examples = [
{ label: 'ASCII', str: 'Hello' },
{ label: 'Кириллица', str: 'Привет' },
{ label: 'Эмодзи', str: '😀🎉🌍' },
{ label: 'Смешанная', str: 'Hi 😀 мир 🌍!' },
{ label: 'Иероглифы', str: '日本語' },
]
for (const { label, str } of examples) {
const realCount = [...str].length
console.log(`${label.padEnd(12)}: length=${str.length}, символов=${realCount}${str.length !== realCount ? ' ⚠️ РАЗНИЦА' : ''}`)
}
// 2. codePointAt vs charCodeAt
console.log('\n=== codePointAt vs charCodeAt ===')
const emoji = '😀'
console.log('charCodeAt(0):', emoji.charCodeAt(0)) // 55357 (суррогат)
console.log('charCodeAt(1):', emoji.charCodeAt(1)) // 56832 (суррогат)
console.log('codePointAt(0):', emoji.codePointAt(0)) // 128512 (0x1F600 — настоящая кодовая точка)
// Восстановить символ из кодовой точки
const cp = emoji.codePointAt(0)
console.log('fromCodePoint:', String.fromCodePoint(cp)) // 😀
// 3. Итерация: for...of корректно обрабатывает суррогатные пары
console.log('\n=== Итерация по символам ===')
const mixed = 'A😀B🌍C'
console.log('Через for...of (правильно):')
const fromFor = []
for (const ch of mixed) {
fromFor.push(ch)
}
console.log(fromFor) // ['A', '😀', 'B', '🌍', 'C'] — 5 символов
console.log('Через индекс (неправильно):')
const fromIndex = []
for (let i = 0; i < mixed.length; i++) {
fromIndex.push(mixed[i])
}
console.log(fromIndex) // 7 элементов — суррогатные пары разбиты
// 4. Разворот строки
console.log('\n=== Разворот строки ===')
const str = 'Привет 🌍'
const badReverse = str.split('').reverse().join('')
const goodReverse = [...str].reverse().join('')
console.log('Оригинал:', str)
console.log('Плохой разворот:', badReverse) // 🌍 ломается
console.log('Правильный разворот:', goodReverse) // корректно
// 5. Нормализация Unicode
console.log('\n=== Нормализация Unicode ===')
const nfc = '\u00E9' // é — один символ (NFC)
const nfd = 'e\u0301' // e + комбинирующий акцент (NFD)
console.log('NFC:', nfc, '| length:', nfc.length) // 1
console.log('NFD:', nfd, '| length:', nfd.length) // 2
console.log('Равны без нормализации:', nfc === nfd) // false
console.log('Равны после normalize:', nfc.normalize('NFC') === nfd.normalize('NFC')) // true
// 6. Корректный подсчёт и усечение
console.log('\n=== countChars и truncate ===')
function countChars(str) {
return [...str].length
}
function truncate(str, maxChars, suffix = '…') {
const chars = [...str]
if (chars.length <= maxChars) return str
return chars.slice(0, maxChars).join('') + suffix
}
const post = 'Добро пожаловать 🎉 в наш чат! Здесь весело 😄🌟'
console.log('UTF-16 units:', post.length)
console.log('Реальных символов:', countChars(post))
console.log('\nОбрезаем до 20 символов:')
console.log(truncate(post, 20))
console.log('Обрезаем до 35 символов:')
console.log(truncate(post, 35))
console.log('Обрезаем до 200 (без изменений):')
console.log(truncate(post, 200))Напиши функцию countChars(str) которая возвращает реальное количество символов (не кодовых единиц UTF-16). Напиши функцию truncate(str, maxChars) которая обрезает строку до maxChars реальных символов и добавляет "…" если строка была обрезана. Напиши функцию reverseString(str) которая корректно разворачивает строку с эмодзи.
countChars: return [...str].length. truncate: const chars = [...str]; return chars.length <= maxChars ? str : chars.slice(0, maxChars).join("") + "…". reverseString: return [...str].reverse().join("")