Пошук уроків, статей та іншого контенту
Розберете Unicode-режим, властивості Unicode та коректну роботу з емодзі й символами різних писемностей.
JavaScript зберігає рядки у форматі UTF-16. Один елемент рядка, доступний через str[i], — це кодова одиниця UTF-16, а не обов’язково повний Unicode-символ.
Наприклад, емодзі 😀 має кодову точку U+1F600, але в UTF-16 представлений сурогатною парою:
const value = "😀";
console.log(value.length); // 2
console.log(value[0]); // �
console.log(value[1]); // �Прапорець u в регулярному виразі вмикає Unicode-режим:
const value = "😀";
console.log(/^.$/.test(value)); // false
console.log(/^.$/u.test(value)); // trueБез u крапка . бачить дві кодові одиниці UTF-16. З u вона бачить одну кодову точку.
Unicode-режим також:
коректно обробляє сурогатні пари;
підтримує Unicode-властивості через \p{...} і \P{...};
змушує JavaScript суворіше перевіряти escape-послідовності;
змінює поведінку квантифікаторів для символів поза базовою багатомовною площиною.
Для регулярних виразів, які працюють із текстом користувача, u майже завжди має бути стандартним вибором.
const musicalSymbol = "\u{1D11E}";
console.log(musicalSymbol.length); // 2 кодові одиниці UTF-16
console.log([...musicalSymbol].length); // 1 кодова точка
const pattern = /^\u{1D11E}$/u;
console.log(pattern.test(musicalSymbol)); // trueБез прапорця u escape-послідовність \u{...} не має такого значення, як у Unicode-режимі.
Unicode містить велику кількість категорій і властивостей символів. JavaScript дозволяє використовувати їх у регулярних виразах.
Основний синтаксис:
/\p{Property=Value}/u
/\p{Binary_Property}/u
/\P{Property=Value}/u\p{...} означає «символ має цю властивість», а \P{...} — «символ не має цієї властивості».
Найчастіше використовуються бінарні властивості та загальні категорії Unicode:
const letter = /^\p{Letter}+$/u;
const number = /^\p{Number}+$/u;
const mark = /^\p{Mark}+$/u;
const punctuation = /^\p{Punctuation}+$/u;
const separator = /^\p{Separator}+$/u;
console.log(letter.test("Привіт")); // true
console.log(letter.test("こんにちは")); // true
console.log(number.test("१२३")); // true
console.log(punctuation.test("!?")); // true
console.log(separator.test(" ")); // trueДля загальних категорій можна використовувати короткі назви:
/^\p{L}+$/u // усі літери
/^\p{N}+$/u // усі числа
/^\p{M}+$/u // усі комбінувальні позначки
/^\p{P}+$/u // пунктуація
/^\p{S}+$/u // символи
/^\p{Z}+$/u // роздільникиІснують також підкатегорії:
/^\p{Lu}+$/u // великі літери
/^\p{Ll}+$/u // малі літери
/^\p{Nd}+$/u // десяткові цифриДля перевірки «літери або цифри» краще явно використовувати властивості:
const alphanumeric = /^[\p{Letter}\p{Number}]+$/u;
console.log(alphanumeric.test("User123")); // true
console.log(alphanumeric.test("Користувач42")); // true
console.log(alphanumeric.test("用户42")); // trueScriptВластивість Script описує писемність, до якої належить символ:
const greek = /^\p{Script=Greek}+$/u;
const cyrillic = /^\p{Script=Cyrillic}+$/u;
const latin = /^\p{Script=Latin}+$/u;
console.log(greek.test("Αθήνα")); // true
console.log(cyrillic.test("Київ")); // true
console.log(latin.test("London")); // trueДля багатьох символів точне значення Script може бути неочевидним. Наприклад, цифри, пунктуація та деякі комбінувальні позначки можуть мати значення Common або Inherited.
У таких випадках зазвичай краще використовувати Script_Extensions, скорочено scx. Ця властивість враховує писемності, у яких символ реально використовується:
const greekText = /^[\p{scx=Greek}\p{White_Space}]+$/u;
console.log(greekText.test("Αθήνα Αθήνα")); // trueПорівняння:
Script — основна писемність символу;
Script_Extensions — усі писемності, у яких символ може використовуватися.
У прикладних задачах scx часто є практичнішим.
Бінарні властивості не мають форми name=value. Наприклад:
const ascii = /^\p{ASCII}+$/u;
const whitespace = /^\p{White_Space}+$/u;
const asciiHex = /^\p{ASCII_Hex_Digit}+$/u;
console.log(ascii.test("Hello")); // true
console.log(ascii.test("Привіт")); // false
console.log(whitespace.test("\n\t ")); // true
console.log(asciiHex.test("A09f")); // trueІнші корисні властивості:
/\p{Alphabetic}/u
/\p{Emoji}/u
/\p{Emoji_Presentation}/u
/\p{Extended_Pictographic}/u
/\p{Default_Ignorable_Code_Point}/u
/\p{White_Space}/u
/\p{Dash}/uНабір підтримуваних властивостей визначається стандартом ECMAScript і версією середовища виконання. Для браузера або Node.js варто перевіряти цільові версії перед використанням рідкісних властивостей.
\w, \d і \s не є повністю Unicode-орієнтованимиУ JavaScript скорочені класи мають обмежене значення:
\d відповідає ASCII-цифрам 0–9;
\w відповідає ASCII-літерам, цифрам, _, а також має спеціальні правила для i та u;
\s відповідає набору пробільних символів, визначеному ECMAScript.
Наприклад:
console.log(/^\d+$/u.test("123")); // true
console.log(/^\d+$/u.test("१२३")); // false
console.log(/^\w+$/u.test("user_123")); // true
console.log(/^\w+$/u.test("користувач")); // falseДля Unicode-тексту не слід замінювати \p{Letter} на \w:
const username = /^[\p{Letter}\p{Number}_]+$/u;
console.log(username.test("користувач_42")); // true
console.log(username.test("用户42")); // trueЯкщо потрібно дозволити лише ASCII, навпаки, \w може бути цілком доречним.
Емодзі — це не окрема проста категорія символів. Частина емодзі складається з одного Unicode-символу, а частина — з послідовностей:
символ плюс варіаційний селектор;
символ плюс модифікатор відтінку шкіри;
прапор із двох регіональних індикаторів;
сімейство з кількох людей;
послідовність із Zero Width Joiner (ZWJ);
емодзі з гендерними або професійними модифікаторами.
Тому такий вираз:
/\p{Emoji}/uможе знайти окремі кодові точки, але не гарантує коректного знаходження цілого видимого емодзі.
const emojiCodePoint = /\p{Emoji}/u;
console.log(emojiCodePoint.test("😀")); // true
console.log(emojiCodePoint.test("A")); // falseВластивість Emoji_Presentation описує символи, які за замовчуванням відображаються як емодзі:
const emojiPresentation = /\p{Emoji_Presentation}/u;
console.log(emojiPresentation.test("😀")); // true
console.log(emojiPresentation.test("©")); // зазвичай falseДеякі символи можуть відображатися як текст або як емодзі залежно від варіаційного селектора:
\uFE0E — текстовий варіант;
\uFE0F — варіант емодзі.
const textCopyright = "©\uFE0E";
const emojiCopyright = "©\uFE0F";
console.log([...textCopyright].length); // 2 кодові точки
console.log([...emojiCopyright].length); // 2 кодові точкиВластивість Extended_Pictographic часто використовується як евристика для пошуку піктографічних символів, зокрема тих, що входять до складених емодзі:
const pictographic = /\p{Extended_Pictographic}/u;
console.log(pictographic.test("👨👩👧👦")); // true
console.log(pictographic.test("🚀")); // trueОднак цей вираз відповідає будь-якій відповідній кодовій точці всередині рядка, а не обов’язково всій послідовності емодзі.
У тексті потрібно розрізняти щонайменше три рівні:
Кодова одиниця UTF-16 — результат str[i].
Кодова точка Unicode — елемент ітерації for...of або [...str].
Графемний кластер — те, що користувач сприймає як один символ.
Приклади:
const samples = [
"😀",
"é",
"e\u0301",
"👨👩👧👦",
"🇺🇦"
];
for (const value of samples) {
console.log({
value,
utf16Units: value.length,
codePoints: [...value].length
});
}Навіть u вирішує лише проблему кодових точок. Воно не робить регулярний вираз графемно-орієнтованим.
Наприклад:
const family = "👨👩👧👦";
console.log([...family].length); // кілька кодових точок
console.log(/^.$/u.test(family)); // falseДля роботи з видимими символами потрібно використовувати сегментацію графемних кластерів через Intl.Segmenter:
const text = "A👨👩👧👦e\u0301🇺🇦";
const segmenter = new Intl.Segmenter("uk", {
granularity: "grapheme"
});
const graphemes = [...segmenter.segment(text)].map(
({ segment }) => segment
);
console.log(graphemes);
// [ "A", "👨👩👧👦", "é", "🇺🇦" ]
console.log(graphemes.length); // 4Це важливо для:
обмеження довжини повідомлення;
курсорів і виділення тексту;
обрізання рядків;
підрахунку символів у полях форм;
коректної обробки емодзі.
Нижче наведено повний приклад для Node.js або сучасного браузера. Він:
знаходить слова Unicode;
визначає скрипти;
знаходить числа;
сегментує рядок на графеми;
визначає графеми, що містять піктографічний символ.
const text = "Привіт, світе! Hello 世界 👨👩👧👦 42 🇺🇦";
const wordPattern = /[\p{Letter}\p{Mark}\p{Number}_]+/gu;
const numberPattern = /^\p{Number}+$/u;
const cyrillicPattern = /^\p{scx=Cyrillic}+$/u;
const latinPattern = /^\p{scx=Latin}+$/u;
const hanPattern = /^\p{scx=Han}+$/u;
const pictographicPattern = /\p{Extended_Pictographic}/u;
const words = text.match(wordPattern) ?? [];
console.log("Токени:", words);
for (const word of words) {
if (numberPattern.test(word)) {
console.log(`${word}: число`);
} else if (cyrillicPattern.test(word)) {
console.log(`${word}: кирилиця`);
} else if (latinPattern.test(word)) {
console.log(`${word}: латиниця`);
} else if (hanPattern.test(word)) {
console.log(`${word}: ієрогліфи Han`);
} else {
console.log(`${word}: змішаний або інший текст`);
}
}
const segmenter = new Intl.Segmenter("uk", {
granularity: "grapheme"
});
const graphemes = [...segmenter.segment(text)].map(
({ segment }) => segment
);
const emojiLikeGraphemes = graphemes.filter((grapheme) =>
pictographicPattern.test(grapheme)
);
console.log("Графеми:", graphemes);
console.log("Графеми з піктограмами:", emojiLikeGraphemes);Регулярний вираз для токенів тут навмисно включає \p{Mark}. Це дозволяє не втратити комбінувальні позначки в словах на кшталт e\u0301.
Однаковий видимий текст може мати різне внутрішнє представлення.
Символ é можна записати:
як одну кодову точку U+00E9;
як e та комбінувальний акцент U+0301.
const composed = "\u00E9";
const decomposed = "e\u0301";
console.log(composed === decomposed); // false
console.log(/^é$/u.test(composed)); // true
console.log(/^é$/u.test(decomposed)); // falseРегулярний вираз не виконує нормалізацію автоматично. Перед порівнянням або перевіркою тексту можна привести його до спільної форми:
const normalizedComposed = decomposed.normalize("NFC");
const normalizedDecomposed = composed.normalize("NFD");
console.log(normalizedComposed === composed); // true
console.log(normalizedDecomposed === decomposed); // trueНайпоширеніші форми:
NFC — канонічна композиція;
NFD — канонічна декомпозиція;
NFKC — сумісна композиція;
NFKD — сумісна декомпозиція.
Для звичайного порівняння тексту часто використовують NFC:
function hasOnlyLetters(value) {
const normalized = value.normalize("NFC");
return /^\p{Letter}+$/u.test(normalized);
}
console.log(hasOnlyLetters("é")); // true
console.log(hasOnlyLetters("e\u0301")); // true
console.log(hasOnlyLetters("Привіт")); // trueNFKC і NFKD можуть змінювати семантичне представлення символів, тому їх не слід застосовувати без чіткого розуміння вимог. Нормалізація також не є повним захистом від схожих символів із різних писемностей.
iПрапорець i вмикає нечутливість до регістру, але для Unicode-тексту результат залежить від правил Unicode case folding.
console.log(/^кИЇВ$/iu.test("Київ")); // true
console.log(/^straße$/iu.test("STRASSE")); // falseОстанній результат очікуваний: нечутливість до регістру не означає повне перетворення всіх можливих еквівалентів у довільний текст. Наприклад, ß і SS мають різну довжину та не обов’язково порівнюються як однакові рядки через регулярний вираз.
Для складного пошуку без урахування регістру краще розглянути попередню обробку або Intl.Collator, а не намагатися описати всі правила вручну регулярним виразом.
v та Unicode SetsУ сучасному ECMAScript існує прапорець v, який розширює можливості Unicode-режиму u.
Він підтримує Unicode Sets — складніші операції над множинами символів, а також властивості Unicode, які можуть описувати послідовності символів. u і v не можна використовувати разом:
const unicodePattern = /^\p{Letter}+$/u;
console.log(unicodePattern.test("Україна")); // trueУ середовищах із підтримкою v можна використовувати, наприклад:
const emojiSequence = /^\p{RGI_Emoji}$/v;
console.log(emojiSequence.test("👨👩👧👦")); // залежить від підтримки vRGI_Emoji — властивість рядків, яка стосується рекомендованих послідовностей емодзі, а не лише окремих кодових точок.
Підтримка v і конкретних властивостей може відрізнятися між версіями браузерів та Node.js. Якщо цільове середовище не гарантоване, використовуйте u разом з Intl.Segmenter і тестуйте потрібні послідовності окремо.
Межі слова \b у JavaScript не перетворюються на повноцінний Unicode-аналізатор слів.
console.log(/\bHello\b/u.test("Hello")); // true
console.log(/\bПривіт\b/u.test("Привіт")); // може не відповідати очікуваннямДля Unicode-словосполучень не слід будувати логіку лише на \b. Краще:
явно визначати допустимі символи через \p{...};
використовувати Intl.Segmenter з granularity: "word";
враховувати правила конкретної мови.
const text = "Привіт, світе! Hello world.";
const segmenter = new Intl.Segmenter("uk", {
granularity: "word"
});
for (const item of segmenter.segment(text)) {
if (item.isWordLike) {
console.log(item.segment);
}
}Властивість \p{Letter} дозволяє приймати літери різних писемностей, але це може створити проблему змішаних скриптів. Наприклад, латинська a та кирилична а виглядають схоже, але є різними символами.
Базова перевірка:
const identifier = /^[\p{Letter}\p{Number}](?:[\p{Letter}\p{Number}_-]{0,31})$/u;
console.log(identifier.test("user_42")); // true
console.log(identifier.test("користувач-42")); // true
console.log(identifier.test("")); // false
console.log(identifier.test("user name")); // falseДля ідентифікаторів, доменів, логінів і платіжних даних слід додатково визначити політику:
чи дозволені різні писемності;
чи можна змішувати скрипти;
чи дозволені комбінувальні позначки;
чи потрібна нормалізація;
чи потрібне виявлення візуально схожих символів.
Регулярний вираз перевіряє синтаксис, але не визначає, чи є рядок безпечним з точки зору візуальної схожості або політики конкретного продукту.
. для підрахунку символівconst value = "👩💻";
console.log(value.length); // не кількість видимих символівНавіть /./u рахує кодові точки, а не графемні кластери. Для видимих символів використовуйте Intl.Segmenter.
u обробляє цілі емодзіu коректно обробляє Unicode-кодові точки, але сімейство 👨👩👧👦 залишається послідовністю кількох кодових точок.
\w для будь-якої мови\w не означає «будь-яка літера Unicode». Для цього використовуйте \p{Letter} або потрібну комбінацію властивостей.
Рядок e\u0301 складається з літери та позначки. Вираз на кшталт \p{Letter}+ не включить \u0301. Для текстових токенів часто потрібна комбінація:
/[\p{Letter}\p{Mark}]+/guВізуально однакові рядки можуть бути різними послідовностями кодових точок. Перед порівнянням узгоджуйте форму через normalize().
\p без u або vВластивості Unicode доступні лише в Unicode-режимах:
const pattern = /^\p{Letter}+$/u;\p{Emoji} може збігатися з окремими символами, які не є повними видимими емодзі. Для кластерів використовуйте Intl.Segmenter, а для підтримуваних середовищ — Unicode Sets із прапорцем v.
u переводить регулярний вираз у режим роботи з Unicode-кодовими точками.
\p{...} і \P{...} дають доступ до Unicode-властивостей.
\p{Letter} кращий за \w, коли потрібні літери різних писемностей.
Script_Extensions часто практичніший за Script.
u не розв’язує проблему графемних кластерів і складених емодзі.
Для підрахунку та сегментації видимих символів використовуйте Intl.Segmenter.
Регулярні вирази не виконують нормалізацію автоматично.
NFC допомагає порівнювати канонічно еквівалентні представлення.
\p{Emoji} знаходить кодові точки, а не обов’язково цілі емодзі-послідовності.
Прапорець v розширює Unicode-можливості, але потребує перевірки підтримки середовищем.
Unicode-валидація синтаксису не замінює перевірку змішаних писемностей, візуально схожих символів і правил безпеки.