Рядки та регулярні вирази
Рядки, можливо, є найбільш важливими типами даних в програмуванні. Вони є майже у всіх високорівневих мовах програмування, а вміння працювати з ними ефективно є необхідністю для розробника, при створенні корисних програм. Втім, будучи відвертими, регулярні вирази є не менш важливими, тому що вони дають розробнику додаткові потужні можливості при роботі з рядками. Зважаючи на це, розробники ECMAScript 6 вдосконалили рядки і регулярні вирази, додавши нові можливості та довгоочікуваний функціонал. Ця глава розгляне зміни в обох типах.
Краща підтримка Unicode
До появи ECMAScript 6, рядки в JavaScript базувались на 16-бітній системі кодування символів. Усі властивості і методи рядків, як, наприклад, властивість length
та метод charAt()
, базувалися на ідеї, що 16-бітна послідовність представляє єдиний символ. ECMAScript 5 дозволяв JavaScript інтерпретаторам обирати поміж двома варіантами кодування: UCS-2 або UTF-16. (Обидві системи використовують послідовності 16-бітових кодових слів (code unites), однаково обробляючи усі відстежуванні операції.) Але вважалося що для кодування символів не потрібно більше ніж 16 біт, на щастя це більше не є абсолютом, дякуючи введенню розширеного набору символів Unicode.
Кодування UTF-16
Обмеження довжини кодування символів до 16 біт не дає можливості реалізувати основну мету Unicode: надання глобального унікального ідентифікатора кожному символу у світі. Ці глобальні унікальні ідентифікатори, так звані коди (code point), просто номери, які починаються з 0.
Коди UTF-16 схожі на коди символів, але між ними існує невелика різниця. Кодування символів перекладає коди в кодові слова, які внутрішньо несуперечливі. В той час як UCS-2 зв’язує коди з кодовими словами у співвідношенні один до одного, зв’язування в UTF-16 відбувається не завжди один до одного.
Перші 2^16 кодів в UTF-16 представлені як одиничні 16-бітні кодові слова. Цей діапазон називається Основна Багатомовна Матриця (ОБМ). Все, що поза межами цього діапазону вважається додатковою матрицею, де кодові слова вже не можуть бути представлені лише у 16 бітах. UTF-16 вирішує цю проблему за допомогою сурогатних пар, в яких один код представлений двома 16-бітними кодовими словами. Це означає, що будь який символ у рядку може бути представлений як одним кодовим словом для ОБМ символів, даючи в сумі 16 біт, так і двома кодовими словами для символів додаткової матриці, даючи в сумі 32 біти.
У ECMAScript 5, всі рядкові операції працюють в діапазоні 16-бітових кодових слів, тому, можна припустити, що ви отримаєте неочікувані результати у рядку кодованому в UTF-16, який містить сурогатні пари. Наприклад:
var text = "𠮷";
console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(text.charAt(0)); // ""
console.log(text.charAt(1)); // ""
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
У цьому прикладі, єдиний Unicode символ представлений сурогатною парою, тому JavaScript операції з рядком відбуваються, як з таким, що має два 16-бітні символи. Це означає:
- властивість
length
змінноїtext
буде 2; - регулярний вираз, який спробує знайти одиничний символ, дасть
false
; - метод
charAt()
не в змозі повернути рядок;
Метод charCodeAt()
повертає відповідний 16-бітний номер для кожного кодового слова, але це тільки наближене до реального значення, яке ви можете отримати в ECMAScript 5.
ECMAScript 6 забезпечує повну підтримку кодування рядків в UTF-16. Стандартизація операцій з рядками, які базуються на цьому кодуванні, означає що JavaScript може підтримувати функціонал розроблений спеціально для роботи з сурогатними парами. Решта цього розділу розглядає кілька ключових прикладів цієї функціональності.
Метод codePointAt()
Одним з методів доданих в ECMAScript 6 для повної підтримки UTF-16 є метод codePointAt()
, який отримує коди Unicode, які пов’язані з відповідною позицією у рядку. Цей метод отримує позицію коду замість позиції символу та повертає числове значення, як показує цей приклад console.log()
:
var text = "𠮷a";
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
console.log(text.charCodeAt(2)); // 97
console.log(text.codePointAt(0)); // 134071
console.log(text.codePointAt(1)); // 57271
console.log(text.codePointAt(2)); // 97
Метод codePointAt()
повертає те саме значення, що й метод charCodeAt()
за винятком того, що він оперує також не-ОБМ символами. Перший символ змінної text
є не-ОБМ символом і він представлений двома кодовими словами; таким чином рядок має довжину трьох символів замість двох. Метод charCodeAt()
повертає тільки перше кодове слово для позиції 0, але codePointAt()
повертає повний код незважаючи на те, що він містить два кодові слова. Обидва методи повертають таке саме значення для позиції 1 (другий кодовий блок для першого символу) та 2 (символ "a"
).
Виклик методу codePointAt()
для символу є найпростішим способом дізнатися складається символ з одного або двох кодових слів. Ось функція, яку ви можете написати для перевірки:
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("𠮷")); // true
console.log(is32Bit("a")); // false
Верхня межа 16-бітових символів представлених в шістнадцятковому вигляді є FFFF
, тому будь-який код більший ніж це число має бути представлений двома кодовими словами, в цілому 32 біта.
Метод String.fromCodePoint()
Тоді як ECMAScript надає можливість щось робити, він також впроваджує спосіб робити те саме у зворотному порядку. Ви можете використати codePointAt()
щоб визначити код для символу в рядку, в той час як String.fromCodePoint()
дає значення символу у рядку відповідно до коду. Наприклад:
console.log(String.fromCodePoint(134071)); // "𠮷"
Слід вважати, що String.fromCodePoint()
— вдосконалена версія String.fromCharCode()
. Обидва дають ті самі результати для символів в межах ОБМ. Різницю ви зможете помітити лише якщо будете працювати з символами за межами ОБМ.
Метод normalize()
Іншим цікавим аспектом Unicode є те, що різні символи можуть вважатися еквівалентними для сортування або інших операцій, що базуються на порівнянні. Є два шляхи визначення цих зв’язків. Перший, канонічна рівність має на увазі, що дві послідовності кодових слів є взаємозамінними у всіх відносинах. Наприклад, комбінація двох символів може бути канонічним еквівалентом одного символу. Друге співвідношення — сумісність. Дві сумісні послідовності кодових слів можуть здаватися різними, але бути взаємозамінними в певних ситуаціях.
Відповідно до цих зв’язків рядки, які виводять з одного боку той самий текст, можуть мати різну послідовність кодових слів. Наприклад символ "æ" та рядок с двох символів "ae" можуть використовуватись однаково, але не бути повністю еквівалентними, поки їх певним чином не нормалізувати.
ECMAScript 6 підтримує нормалізацію форм Unicode, передаючи рядку метод normalize()
. Цей метод опціонально приймає один параметр у вигляді рядка, який має містити одну з наступних форм Unicode нормалізації для подальшого використання:
"NFC"
(Normalization Form Canonical Composition) — форма нормалізації «Канонічна Композиція»; використовується за замовчуванням"NFD"
(Normalization Form Canonical Decomposition) — форма нормалізації «Канонічна Декомпозиція»;"NFKC"
(Normalization Form Compatibility Composition) — форма нормалізації «Сумісна Композиція»;"NFKD"
(Normalization Form Compatibility Decomposition) — форма нормалізації «Сумісна Декомпозиція».
Пояснення відмінностей між цими чотирма формами виходить за межі цієї книги. Тільки майте на увазі, що коли порівнюєте рядки, обидва мають бути нормалізовані до однієї форми. Наприклад:
var normalized = values.map(function(text) {
return text.normalize();
});
normalized.sort(function(first, second) {
if (first < second) {
return -1;
} else if (first === second) {
return 0;
} else {
return 1;
}
});
Цей код конвертує рядки в масив values
у нормалізованій формі, таким чином масив може бути правильно відсортований. Ви також можете відсортувати оригінальний масив, використовуючи метод normalize()
як частину умови, наприклад:
values.sort(function(first, second) {
var firstNormalized = first.normalize(),
secondNormalized = second.normalize();
if (firstNormalized < secondNormalized) {
return -1;
} else if (firstNormalized === secondNormalized) {
return 0;
} else {
return 1;
}
});
Повторимо ще раз, що найважливішим в цьому коді є те, що обидва аргументи, first
та second
, будуть нормалізовані однаковим чином. Ці приклади використовують форму нормалізації за замовчуванням, NFC, але ви можете легко визначити іншу, наприклад:
values.sort(function(first, second) {
var firstNormalized = first.normalize("NFD"),
secondNormalized = second.normalize("NFD");
if (firstNormalized < secondNormalized) {
return -1;
} else if (firstNormalized === secondNormalized) {
return 0;
} else {
return 1;
}
});
Якщо ви не турбувалися про нормалізацію Unicode раніше, тоді, напевно, цей метод не буде мати особливого значення. Але якщо ви колись будете працювати з кодом для інтернаціональних програм, метод normalize()
стане вам у нагоді.
Методи — не єдині покращення, які ECMAScript 6 впроваджує для роботи з рядками Unicode. Стандарт також пропонує два нові елементи синтаксису.
Опція пошуку (flag) u в Регулярних Виразах
За допомогою регулярних виразів ви можете виконати багато базових операцій з рядками. Але треба пам’ятати, що регулярні вирази використовують 16-бітові кодові слова, де кожен представляє один символ. Щоб зарадити цій проблемі, ECMAScript 6 вводить опцію пошуку u
для регулярних виразів, які працюють з Unicode.
Якщо регулярний вираз має опцію u
, то він переключається в стан роботи з символами, а не з кодовими словами. Це означає, що регулярний вираз вже, незалежно від сурогатних пар в рядку, буде поводитись як треба. Як приклад, розглянемо цей код:
var text = "𠮷";
console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(/^.$/u.test(text)); // true
Регулярний вираз /^.$/
не знаходить жодного рядка, який би складався з одного символу. Використаний без опції u
, цей регулярний вираз порівнює кодові слова, тому японський символ (якій представлений двома кодовими словами) не відповідає регулярному виразу. Коли ж використовується опція u
, регулярний вираз порівнює символи замість кодових слів і таким чином японський символ відповідає виразу.
На жаль, ECMAScript 6 не може визначити скільки кодових пунктів містить рядок, але з визначеною опцією u
, ви можете використати регулярний вираз, щоб реалізувати це таким чином:
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
console.log(codePointLength("abc")); // 3
console.log(codePointLength("𠮷bc")); // 3
У цьому прикладі використовується match()
, щоб перевірити text
на символи пробілів і не пробілів, за допомогою регулярного виразу, який застосовано глобально та з підтримкою Unicode. Результат містить масив збігів, якщо наявний хоча б один збіг, то довжина масиву буде числом кодів у рядку. В Unicode, рядки "abc"
та "𠮷bc"
мають три символи, тому довжина масиву буде три.
W> Даний підхід працює, але не дуже швидко, особливо коли його застосувати до довгих рядків. Тому намагайтеся зменшити підрахунок кодів, якщо це можливо. На щастя, ECMAScript 7 буде мати вбудований метод підрахунку кодів.
Оскільки опція u
є синтаксичною зміною, спроби використання її в JavaScript інтерпретаторах, які не сумісні з ECMAScript 6 будуть провокувати синтаксичну помилку. Найбезпечнішим шляхом встановити, чи підтримується опція u
буде функція, на кшталт цієї:
function hasRegExpU() {
try {
var pattern = new RegExp(".", "u");
return true;
} catch (ex) {
return false;
}
}
Ця функція використовує конструктор RegExp
, щоб передати опцію u
як аргумент. Такий синтаксис підтримується навіть старими JavaScript інтерпретаторами, але конструктор буде видавати помилку, якщо u
не підтримується.
I> Якщо потрібно, щоб ваш код працював зі старими JavaScript інтерпретаторами, завжди використовуйте конструктор RegExp
з використанням опції u
. Це попередить виникнення синтаксичних помилок і дозволить визначити чи підтримується опція u
без скасування виконання коду.
Інші зміни для рядків
Функціонал рядків JavaScript завжди відставав від аналогічного в інших мовах. Тільки в ECMAScript 5 рядки, нарешті, отримали метод trim()
. ECMAScript 6 продовжує розвивати функціонал JavaScript для роботи з рядками.
Методи для визначення підрядків
З того часу, коли JavaScript був вперше представлений, розробники використовували метод indexOf()
щоб визначити рядок всередині рядка. ECMAScript 6 містить наступні три методи для реалізації цієї дії:
- Метод
includes()
повертаєtrue
, якщо даний тест знайдений деінде у рядку. Та повертаєfalse
, якщо ні. - Метод
startsWith()
повертаєtrue
, якщо даний текст знайдено на початку рядка. Та повертаєfalse
, якщо ні. - Метод
endsWith()
повертаєtrue
, якщо даний текст знайдено у кінці рядка. Та повертаєfalse
, якщо ні.
Кожен з цих методів приймає два аргументи: текст, який треба знайти, та необов'язковий аргумент у вигляді індексу рядка з якого треба шукати. Коли надано другий аргумент, includes()
та startsWith()
починає пошук з вказаного індексу, в той час як endsWith()
починає шукати з індексу, який рівний довжині рядка мінус вказаний аргумент; коли другий аргумент не надано, includes()
та startsWith()
шукають з початку рядка, в той час як endsWith()
починає з кінця. Кажучи інакше, другий аргумент зменшує діапазон пошуку в рядку. Ось кілька прикладів цих методів в дії:
var msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
console.log(msg.endsWith("!")); // true
console.log(msg.includes("o")); // true
console.log(msg.startsWith("o")); // false
console.log(msg.endsWith("world!")); // true
console.log(msg.includes("x")); // false
console.log(msg.startsWith("o", 4)); // true
console.log(msg.endsWith("o", 8)); // true
console.log(msg.includes("o", 8)); // false
В перших трьох викликах другий аргумент не вказано, тож пошук ведеться по всій довжині рядка. Останні три виклики перевіряють лише частину рядка. Виклик msg.startsWith("o", 4)
починає шукати з індексу 4 змінної msg
(що є "o" в "Hello"); виклик msg.endsWith("o", 8)
починає шукати з індексу 4, тому що аргумент 8
віднімаємо від довжини рядка (12); виклик msg.includes("o", 8)
починає шукати з індексу 8 (що буде "r" в "world").
Не зважаючи на те, що ці методи роблять визначення підрядка у рядку легшим, кожен з них повертає лише булеве значення. Якщо вам потрібно знайти дійсну позицію підрядка в рядку, треба використовувати методи indexOf()
або lastIndexOf()
.
W> Методи startsWith()
, endsWith()
та includes()
будуть видавати помилку, якщо ви передасте регулярний вираз замість рядка в якості аргументу. На відміну від indexOf()
та lastIndexOf()
, які конвертують регулярний вираз в рядок а потім шукають цей рядок.
Метод repeat()
ECMAScript 6 також додає до рядків метод repeat()
, який в якості аргументу приймає число рівне кількості повторів рядка. Він повертає новий рядок, який містить оригінальний рядок повторений вказану кількість разів. Наприклад:
console.log("x".repeat(3)); // "xxx"
console.log("hello".repeat(2)); // "hellohello"
console.log("abc".repeat(4)); // "abcabcabcabc"
Цей метод надає важливий функціонал, який може бути надзвичайно корисним при маніпулюванні текстом. Це важливо в інструментах для форматування коду, наприклад:
// відступ, використовуючи визначену кількість пробілів
var indent = " ".repeat(4),
indentLevel = 0;
// кожного разу, коли ви збільшуєте відступ
var newIndent = indent.repeat(++indentLevel);
Перший виклик repeat()
створить рядок з чотирма пробілами, а змінна indentLevel
буде записувати рівень відступів. Тепер ви можете просто викликати repeat()
зі збільшеним indentLevel
, щоб змінити кількість відступів.
ECMAScript 6 також надає деякі корисні зміни до регулярних виразів, які не можна виділити в окрему категорію. Наступний розділ розгляне деякі з них.
Інші зміни у регулярних виразах
Регулярні вирази важлива частина роботи з рядками в JavaScript, і як більша частина мови, вони не змінювались істотним чином у попередніх версіях. ECMAScript 6 пропонує деякі покрашення для регулярних виразів, принаймні, щоб йти поруч з рядками.
Опція (flag) y
для регулярних виразів
ECMAScript 6 зробив опцію y
стандартом flag після того, як вона була запроваджена в Firefox в якості пропрієтароного доповнення для опрацювання регулярних виразів. Опція y
стосується такої властивості регулярних виразів, як sticky
, яка каже пошуку почати шукати відповідні символи в рядку з позиції, зазначеної у властивості lastIndex
регулярного виразу. Якщо в цій позиції немає збігів, то регулярний вираз зупиняє пошук відповідностей. Щоб побачити, як це працює, розглянемо наступний код:
var text = "hello1 hello2 hello3",
pattern = /hello\d\s?/,
result = pattern.exec(text),
globalPattern = /hello\d\s?/g,
globalResult = globalPattern.exec(text),
stickyPattern = /hello\d\s?/y,
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello1 "
console.log(stickyResult[0]); // "hello1 "
pattern.lastIndex = 1;
globalPattern.lastIndex = 1;
stickyPattern.lastIndex = 1;
result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello2 "
console.log(stickyResult[0]); // Error! stickyResult is null
Цей приклад має три регулярні вирази. Вираз в pattern
немає опцій, другий в globalPattern
використовує опцію g
, останній stickyPattern
використовує опцію y
. У перших трьох викликах console.log()
, усі три регулярні вирази мають повернути "hello1 "
(з пробілом у кінці).
Після цього властивість lastIndex
було змінено на 1 у всіх трьох шаблонах, маючи на увазі, що регулярний вираз повинен шукати збіги з другого символу у всіх випадках. Регулярний вираз без опцій повністю ігнорує зміни у lastIndex
та все ще повертає "hello1 "
без проблем. Регулярний вираз з опцією g
повертає збіг з "hello2 "
, тому що він починає пошук з другого символу рядка ("e"
). Регулярний вираз з опцією y
не знаходить жодних збігів, починаючи з другого символу рядка, тому stickyResult
є null
.
Опція y
зберігає індекс наступного символу після останнього в lastIndex
під час виконання операції. Якщо в результаті операції немає збігів, тоді lastIndex
повертається до 0. Опція g
поводиться таким чином, як показано тут:
var text = "hello1 hello2 hello3",
pattern = /hello\d\s?/,
result = pattern.exec(text),
globalPattern = /hello\d\s?/g,
globalResult = globalPattern.exec(text),
stickyPattern = /hello\d\s?/y,
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello1 "
console.log(stickyResult[0]); // "hello1 "
console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 7
console.log(stickyPattern.lastIndex); // 7
result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);
console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello2 "
console.log(stickyResult[0]); // "hello2 "
console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 14
console.log(stickyPattern.lastIndex); // 14
Значення lastIndex
змінюється на 7 після першого виклику exec()
та на 14 після другого виклику, як для змінної stickyPattern
так і для globalPattern
.
Є дві важливі деталі, які треба мати на увазі стосовно опціі y
:
- Властивість
lastIndex
буде враховуватися тільки при використанні з методами, які існують для регулярних виразів, на кшталтexec()
абоtest()
. Передавання регулярного виразу до рядкового методу, якmatch()
, не поверне результату. - Коли ми використовуємо символ
^
щоб почати пошук з початку рядка, регулярний вираз з опцієюy
шукає збіги тільки з початку рядка (або початку лінії в багатолінійному коді). ЯкщоlastIndex
є 0, символ^
робить регулярний вираз таким самим як і загального типу. ЯкщоlastIndex
не відповідає початку рядка або початку рядка в багаторядковому режимі, регулярний вираз з опцієюy
ніколи не поверне збіг.
Так само як і з іншими опціями регулярних виразів, ви можете визначити наявність опції y
, використовуючи властивість sticky
. В цьому разі ви маєте перевірити наявність властивості sticky
, як показано у коді:
var pattern = /hello\d/y;
console.log(pattern.sticky); // true
Перевірка на властивість sticky
буде повертати true
, якщо опція y
наявна у виразі, та false
, якщо ні. Властивість sticky
є доступною тільки для читання і не може буди зміненою в коді.
Так само як опція u
, опція y
є синтаксичною зміною, тому вона буде викликати синтаксичну помилку у старих інтерпретаторах JavaScript. Ви можете використати наступний підхід для перевірки підтримки:
function hasRegExpY() {
try {
var pattern = new RegExp(".", "y");
return true;
} catch (ex) {
return false;
}
}
Так само як перевірка на опцію u
, код повертає false
, якщо не в змозі створити регулярний вираз з опцією y
. Подібно до використання u
, якщо вам треба використати y
в коді, який обробляється у старих інтерпретаторах JavaScript, будьте певними, що використовуєте конструктор RegExp
коли визначаєте регулярний вираз, щоб уникнути помилок.
Дублювання Regular Expressions
В ECMAScript 5, ви можете дублювати регулярні вирази, передавши до конструктора RegExp
таким чином:
var re1 = /ab/i,
re2 = new RegExp(re1);
Змінна re2
є звичайною копією змінної re1
. Але якщо ви передасте другий аргумент до конструктору RegExp
, який буде визначати опцію для вашого регулярного виразу, ви отримаєте помилку, як в цьому прикладі:
var re1 = /ab/i,
// видає помилку в ES5, працює в ES6
re2 = new RegExp(re1, "g");
Якщо ви виконаєте цей код в оточенні ECMAScript 5, ви отримаєте помилку, яка вказуватиме на те, що другий аргумент не може бути використаний, якщо перший аргумент є регулярним виразом . ECMAScript 6 змінює цю поведінку таким чином, що другий аргумент є дозволеним та буде переписувати будь-яку опцію, яка буде міститися у першому аргументі. Наприклад:
var re1 = /ab/i,
// видає помилку в ES5, працює в ES6
re2 = new RegExp(re1, "g");
console.log(re1.toString()); // "/ab/i"
console.log(re2.toString()); // "/ab/g"
console.log(re1.test("ab")); // true
console.log(re2.test("ab")); // true
console.log(re1.test("AB")); // true
console.log(re2.test("AB")); // false
В цьому коді, re1
має не чутливу до регістру опцію i
, в той час як re2
має тільки опцію g
. Конструктор RegExp
дублює вираз по шаблону re1
та змінює опцією g
на опцію i
. Без другого аргументу, re2
буде мати ті самі опції, що й re1
.
Властивість flags
Разом з доданням нової опції та зміни засобів роботи з опціями, ECMAScript 6 додає нову властивість пов’язану з ними. В ECMAScript 5 ви могли отримати текст регулярного виразу, використовуючи властивість source
, але щоб отримати рядкове представлення опції, вам було потрібно парсити результат метода toString()
, як показано нижче:
function getFlags(re) {
var text = re.toString();
return text.substring(text.lastIndexOf("/") + 1, text.length);
}
// toString() є "/ab/g"
var re = /ab/g;
console.log(getFlags(re)); // "g"
Цей код конвертує регулярний вираз в рядок, а потім повертає символи знайдені після останнього /
. Ці символ і є опції.
ECMAScript 6 робить визначення опцій легше, додаючи властивість flags
до існуючої властивості source
. Обидві властивості є засобами доступу до властивостей прототипу, що робить їх доступними тільки до зчитування. Властивість flags
робить опрацювання регулярних виразів легшим, як для налагоджування так і для успадкування.
Додана до ECMAScript 6 в останню чергу, властивість flags
повертає рядкове відображення будь-якої опції доданої до регулярного виразу, наприклад:
var re = /ab/g;
console.log(re.source); // "ab"
console.log(re.flags); // "g"
Код отримує всі опції від re
та виводить їх до консолі значно легшим шляхом в той час, коли техніка це робить з використанням методу toString()
. Використання source
та flags
разом дозволить вам отримувати частини регулярного виразу без використання технік з парсингом.
Всі зміни до рядків і регулярних виразів, які розглянуті в цьому розділі є безумовно потужними, але ECMAScript 6 надає ширші можливості роботи з рядками. Він додає новий літерал до таблиці, що робить рядки більш гнучкими.
Шаблонні літерали
Рядки в JavaScript's завжди були досить обмеженими в порівнянні з іншими мовами. З початку становлення JavaScript, рядкам не вистачало методів розглянутих вище в цьому розділі, а конкатенація рядків є досить простою. Шаблонні літерали додають новий синтаксис для створення специфічної для домену мови (DSLs), щоб працювати з контентом у більш безпечний спосіб ніж ми робимо це зараз. DSLs мова розроблена для специфічного, вузького використання (на відміну від JavaScript, який є мовою широкого використання) і можливість створювати DSLs в середені JavaScript була дуже бажаною для розробників JavaScript, для розв’язання найбільш складних проблем. Вікі ECMAScript пропонує наступне визначення для template literal strawman:
Ця схема розширює синтаксис ECMAScript додаванням синтаксичного цукру, щоб дозволити іншим бібліотекам впроваджувати DSLs для легкого створення запитів та маніпулювання контентом з інших мов, які стійкі для ін’єкцій та атак, як XSS, SQL Ін’єкції, та інші.
Насправді, шаблонні літерали ECMAScript 6 — це відповідь усім недолікам, які мав JavaScript в цьому плані з моменту виходу ECMAScript 5:
- багаторядковий режим формальна концепція багатолінійних рядків;
- базове форматування рядків можливість заміщувати частину рядка значеннями з певних змінних;
- HTML escaping можливість трансформувати рядки, щоб безпечно вставляти іх в HTML.
Замість того, щоб додати більше можливостей існуючому функціоналу з опрацювання рядків JavaScript, літерали рядків пропонують повністю новий підхід для вирішення цих проблем.
Загальний синтаксис
Дякуючи своїй простоті, шаблонні літерали працюють як звичайні рядки оточенні зворотніми лапками (`
) замість подвійних або одинарних. Розглянемо цей приклад:
let message = `Hello world!`;
console.log(message); // "Hello world!"
console.log(typeof message); // "string"
console.log(message.length); // 12
Цей код показує, що змінна message
містить звичайний рядок JavaScript. В даному випадку синтаксис шаблонного літералу використано тільки для того, щоб створити рядкове значення, яке згодом буде прив’язане до змінної message
.
Якщо ви хочете використовувати зворотні лапки й надалі, тоді треба екранувати їх зворотнім слешем (\
), як в цьому варіанті змінної message
:
let message = `\`Hello\` world!`;
console.log(message); // "`Hello` world!"
console.log(typeof message); // "string"
console.log(message.length); // 14
Ви не повинні екранувати подвійні або одинарні лапки в синтаксисі шаблонного літералу.
Багаторядковий режим
JavaScript розробники шукали можливість працювати в багаторядковому режимі з моменту створення мови. Але коли ми використовуємо подвійні або одинарні лапки, рядок має бути розташований тільки на одному рядку коду.
Обхідні шляхи до появи ECMAScript 6
Дякуючи давно відомому синтаксичному багу, JavaScript має обхідні шляхи. Ви можете працювати в багаторядковому режимі, якщо перед новим рядком коду ставити зворотній слеш (\
). Ось, наприклад:
var message = "Multiline \
string";
console.log(message); // "Multiline string"
Рядок message
не має нових рядків в консолі, тому що зворотній слеш сприймаєтеся як продовження поточного рядка, а не початок нового. Для того щоб визначити новий рядок, вам потрібно його позначити:
var message = "Multiline \n\
string";
console.log(message); // "Multiline
// string"
Це має вивести Multiline String
на двох роздільних рядках у більшості JavaScript інтерпретаторів, але, по суті, така поведінка визначається як баг, тому більшість розробників радять не користатися таким трюком.
Іншими шляхами працювати у багаторядковому режимі до появи ECMAScript 6 було звернення до масивів або конкатенація рядків, наприклад:
var message = [
"Multiline ",
"string"
].join("\n");
let message = "Multiline \n" +
"string";
Проте у цих всіх обхідних шляхах не вистачало того, що було потрібно розробникам.
Багаторядковий режим простим чином
Шаблонні літерали ECMAScript 6 роблять роботу у багаторядковому режимі досить легкою, тому що не мають спеціального синтаксису. Просто робіть новий рядок де вам треба і він буде оброблений. Наприклад:
let message = `Multiline
string`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 16
Усі пробіли всередині зворотних лапок є частиною рядка, тому будьте уважними з відступами. Наприклад:
let message = `Multiline
string`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 31
У цьому коді, всі пробіли перед другим рядком шаблону фактично вважаються частиною самого рядка. Якщо зробити текстовий рядок з правильними відступами для вас важливо, то потрібно залиши порожнім перший рядок у багаторядковому шаблонному літералі, а потім починати робити відступи в нових рядках, а саме:
let html = `
<div>
<h1>Title</h1>
</div>`.trim();
Цей код починає шаблонний літерал на першому рядку, але він не має ніякого тексту аж до другого. Теги HTML мають відступи для гарного вигляду, а потім метод trim()
викликається щоб видалити перший порожній рядок.
A> Якщо ви бажаєте, ви також можете використовувати символ \n
в шаблонному літералі, щоб показати де має бути створена нова лінія:
let message = `Multiline\nstring`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 16
Робимо підстановки
В цьому сенсі, шаблонні літерали можуть здаватися більш вдосконаленою версією звичайних JavaScript рядків. Реальна різниця між ними якраз і міститься в шаблонному літералі підстановки. Підстановки дозволяють вам помістити любий валідний JavaScript вираз в середину шаблонного літералу і вивести результат як частину рядка.
Підстановки оточені відкриваючим ${
і закриваючим }
, що може містити будь-який JavaScript вираз. Найпростіша підстановка дозволить вам помістити локальні змінні в підсумковий рядок, наприклад:
let name = "Nicholas",
message = `Hello, ${name}.`;
console.log(message); // "Hello, Nicholas."
Підстановка в ${name}
має доступ до локальної змінної name
щоб вставити name
в рядок message
. Змінна message
одразу ж виводить результат підстановки.
I> Шаблонний літерал може мати доступ до будь-якої наявної змінної в області видимості, до якої він належить. Спроба використати в шаблонному літералі не визначену змінну призведе до помилки як у строгому, так і нестрогому режимі.
Оскільки всі підстановки є JavaScript виразами, ви можете підставляти не тільки прості імена змінних. Ви можете легко використати результати обчислень або функцій. Наприклад:
let count = 10,
price = 0.25,
message = `${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message); // "10 items cost $2.50."
Цей код виконує обчислення, як частину шаблонного літералу. До змінних count
та price
застосовується операція множення, щоб отримати результат, а потім форматування до двох символів після коми за допомогою .toFixed()
. Знак долару перед другою підстановкою виводиться як є, тому що після нього немає відкриваючої фігурної дужки.
Теговані шаблони
Ви побачили як шаблонні літерали можуть створювати багаторядкові рядки та вставляти значення в рядки без конкатенації. Але справжню силу шаблонних літералів можна відчути з тегованими шаблонами. Тег шаблону виконує трансформацію шаблонного літералу і повертає остаточне значення рядка. Такий тег визначається на початку рядка, одразу перед першим символом `
, як показано тут:
let message = tag`Hello world`;
В цьому прикладі, tag
є тегом шаблону щоб застосувати шаблонного літералу `Hello world`
.
Визначаємо теги
Насправді тег це просто функція яка виконується при обробці даних шаблонного літералу. Тег отримує данні про шаблонні літерали як окремі частини коду і має зібрати ці частини разом. Перший аргумент — це масив рядків шаблонного літералу як їх інтерпретує JavaScript. Кожен наступний аргумент — це відтворене значення кожної підстановки.
Функції тегів як правило викликаються з аргументами, як показано нижче, щоб полегшити роботу з даними:
function tag(literals, ...substitutions) {
// повертає рядок
}
Щоб краще зрозуміти що передається до тегів, розглянемо наступне:
let count = 10,
price = 0.25,
message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
Якщо ви матимете функцію з назвою passthru()
, то вона отримає три аргументи. По-перше, вона отримає масив literals
, який матиме наступні елементи:
- пустий рядок перед першою підстановкою (
""
); - рядок після першої і перед другою підстановкою (
" items cost $"
); - рядок після другої підстановки (
"."
).
Наступний аргумент буде 10
, що є значенням змінної count
. Він стає першим елементом в масиві substitutions
. Останнім аргументом буде "2.50"
, що є здобутим значенням для (count * price).toFixed(2)
та другого елементу в масиві substitutions
.
Зауважте, що перший елемент в literals
є порожнім рядком. Таким чином ми впевнені, що literals[0]
є завжди початком рядка, так само як literals[literals.length - 1]
завжди кінець рядка. Підстановок завжди на одну менше ніж літералів, таким чином вираз substitutions.length === literals.length - 1
завжди правильний.
Використовуючи цей шаблон, масиви literals
та substitutions
можуть бути зв’язані, щоб утворити результуючий рядок. Перший елемент в literals
йде першим, перший елемент substitutions
йде за ним, і так далі, поки рядок не буде опрацьовано. Наприклад, ви можете імітувати поведінку літералу шаблону, чергуючи значення цих двох масивів:
function passthru(literals, ...substitutions) {
let result = "";
// запускаємо цикл тільки для підрахунку substitutions
for (let i = 0; i < substitutions.length; i++) {
result += literals[i];
result += substitutions[i];
}
// додаємо осатаній літерал
result += literals[literals.length - 1];
return result;
}
let count = 10,
price = 0.25,
message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message); // "10 items cost $2.50."
В цьому прикладі визначається тег passthru
який виконує ту ж саму трансформацію що й шаблонний літерал за замовчуванням. Єдина хитрість тут — це використання substitutions.length
для циклу замість literals.length
, щоб уникнути ненавмисного виходу за рамки масиву substitutions
. Це працює тому, що відношення між literals
та substitutions
добре визначені в ECMAScript 6.
I> Значення в substitutions
не обов'язково мають бути рядками. Якщо у виразі виконується число, як в попередньому прикладі, тоді буде передаватися числове значення. Визначення кількості значень, що мають бути виведені в результаті частини роботи тегів.
Використання первинних значень у шаблонних літералах
Теги шаблонів також мають доступ до первинної інформації, що в першу чергу означає доступ до символів екранування перш ніж вони будуть трансформовані в їх символьні еквіваленти. Найпростішим засобом для роботи з первинними значеннями рядків є використання вбудованого тегу String.raw()
. Наприклад:
let message1 = `Multiline\nstring`,
message2 = String.raw`Multiline\nstring`;
console.log(message1); // "Multiline
// string"
console.log(message2); // "Multiline\\nstring"
В цьому коді, \n
в message1
інтерпретується як символ нової лінії, в той час як \n
в message2
інтерпретується в його первинній формі "\\n"
(символи слешу й n
). Доступ до первинної інформації, як в цьому прикладі, дозволяє виконувати більш комплексні операції за необхідності.
Первинна інформація рядка також передається в теги шаблону. Перший аргумент в функції тегу — масив з екстра властивістю названою raw
. Властивість raw
є масивом який вміщає первинний еквівалент кожного значення літералу. Наприклад, значення в literals[0]
завжди має еквівалент literals.raw[0]
, який містить інформацію рядка. Знаючи це, ви можете імітувати String.raw()
, використовуючи наступний код:
function raw(literals, ...substitutions) {
let result = "";
// запускаємо цикл тільки для підрахунку substitutions
for (let i = 0; i < substitutions.length; i++) {
result += literals.raw[i]; // use raw values instead
result += substitutions[i];
}
// додаємо останній літерал
result += literals.raw[literals.length - 1];
return result;
}
let message = raw`Multiline\nstring`;
console.log(message); // "Multiline\\nstring"
console.log(message.length); // 17
Код використовує literals.raw
замість literals
щоб вивести результуючий рядок. Це означає, що будь-які символи, що екрануються, в тому числі коди Unicode, мають бути повернені в їх первинній формі. Первинна форма рядків стає у нагоді, коли ви хочете вивести рядок, який містить код в якому ви хочете вивести екрановані символи (наприклад, коли ви хочете генерувати документацію про якийсь код, ви, можливо, захочете вивести код у такому вигляді як він є).
Підсумок
Повна підтримка Unicode дозволяє JavaScript працювати з символами UTF-16 логічним чином. Можливість переходів між кодами та символами через codePointAt()
та String.fromCodePoint()
є важливим кроком в маніпуляціях з рядками. Додавання опції u
до регулярних виразів дає можливість оперувати кодами замість 16-бітових символів, а метод normalize()
дозволяє краще порівнювати рядки.
ECMAScript 6 також додає нові методи для роботи з рядками, дозволяючи вам краще визначати субрядки, дивлячись на їх позицію в батьківському рядку. Також більше функціоналу було надано регулярним виразам.
Шаблонні літерали є важливим додатком до ECMAScript 6, що дозволяє вам створювати специфічні до домену мови (DSLs), щоб полегшити створення рядків. Можливість вставляти змінні безпосередньо в шаблонний літерал означає, що розробники мають безпечніший інструмент ніж конкатенація рядків при поєднанні довгих рядків зі змінними.
Вбудована підтримка багаторядкових рядків також робить шаблонні літерали корисним доповненням до рядків JavaScript, які досі не мали такої можливості. Не зважаючи на можливість використання нових рядків безпосередньо всередині шаблонного літералу, ви все ще можете використовувати \n
та інші екрановані символи.
Теги шаблону є найбільш важливою частиною цього покращення для створення DSLs. Теги — це функції, що отримують частини літералу шаблону як аргументи. В подальшому ви можете використовувати ці данні, щоб повернути відповідне значення рядка. Впроваджені данні включають літерали, їхні вихідні еквіваленти, та будь−які значення заміщень. Ці частини інформації в подальшому можуть буди використані, щоб визначити коректний вивід тегу.