16 липня 2023 р.

Рядки

У JavaScript текстові дані зберігаються у вигляді рядків. Не існує окремого типу для одного символу.

Внутрішній формат для рядків завжди UTF-16, він не привʼязаний до кодування сторінки.

Лапки

Згадаймо види лапок.

Рядки можуть бути включені в одинарні лапки, подвійні лапки або зворотні знаки:

let single = 'одинарні-лапки';
let double = "подвійні-лапки";

let backticks = `зворотні-лапки`;

Одинарні та подвійні лапки по суті однакові. Однак зворотні лапки дозволяють нам вставляти будь-який вираз у рядок, загортаючи його у ${…}:

function sum(a, b) {
  return a + b;
}

alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.

Ще однією перевагою використання зворотних лапок є те, що вони дозволяють рядку охоплювати кілька ліній:

let guestList = `Гості:
 * Іван
 * Петро
 * Марія
`;

alert(guestList); // список гостей в кілька рядків

Виглядає природно, правда? Але одинарні або подвійні лапки так не працюють.

Якщо ми спробуємо їх використати в кілька рядків, буде помилка:

let guestList = "Гості: // Помилка: Unexpected token ILLEGAL
  * Іван";

Одинарні та подвійні лапки беруть свій початок з давніх часів створення мови, коли не було потреби у багатолінійних рядках. Зворотні лапки зʼявилися набагато пізніше і тому є більш універсальними.

Зворотні лапки також дозволяють нам задати “шаблонну функцію” перед першими зворотніми лапками. Синтаксис такий: func`string`. Функція func викликається автоматично, отримує рядок і вбудовані в неї вирази і може їх обробити. Це називається “теговим шаблоном”, це рідко використовується на практиці, але ви можете прочитати детальніше про це на MDN: Template literals.

Спеціальні символи

Ще можна створити багатолінійні рядки з одинарними та подвійними лапками за допомогою так званого “символу нового рядка”, записаного як \n, який позначає розрив рядка:

let guestList = "Гості:\n * Іван\n * Петро\n * Марія";

alert(guestList); // список гостей в декілька рядків, як і вище

Наприклад, ці два рядки рівнозначні, просто написані по-різному:

let str1 = "Привіт\nСвіт"; // два рядки з використанням "символу нового рядка"

// два рядки з використанням звичайного нового рядка та зворотних лапок
let str2 = `Привіт
Світ`;

alert(str1 == str2); // true

Є й інші, менш поширені “спеціальні” символи.

Символ Опис
\n Розрив рядка
\r У текстових файлах Windows комбінація двох символів \r\n являє собою розрив рядка, тоді як в інших ОС, це просто \n. Так склалось з історичних причин, більшість ПЗ під Windows також розуміє \n
\'\"\` Лапки
\\ Зворотний слеш
\t Знак табуляції
\b, \f, \v Backspace, Form Feed, Vertical Tab – зберігаються для зворотної сумісності, зараз не використовуються

Усі спеціальні символи починаються зі зворотного слеша \. Його також називають “символом екранування”.

Оскільки це так особливо, якщо нам потрібно показати зворотний слеш \ у рядку, нам потрібно подвоїти його:

alert( `Зворотний слеш: \\` ); // Зворотний слеш: \

Так звані “екрановані” лапки \', \", \` використовуються для вставки цих лапок в рядок, який обмежено таким же типом лапок.

Наприклад:

alert( 'Ім\'я моє — Морж!' ); // Ім'я моє — Морж!

Як бачите, ми повинні “екранувати” лапку зворотним слешем \', оскільки інакше це означало б кінець рядка.

Звісно, потрібно “екранувати” лише такі лапки, якими обрамлений рядок. Як елегантніше рішення, ми могли б замість цього скористатися подвійними або зворотними лапками:

alert( `Ім'я моє — Морж!` ); // Ім'я моє — Морж!

Окрім цих спеціальних символів, існує також спеціальна нотація для кодів Unicode \u…, вона використовується рідко та описана в додатковому розділі про Unicode.

Довжина рядка

Властивість length містить в собі довжину рядка:

alert( `Моє\n`.length ); // 4

Зверніть увагу, що \n – це один спеціальний символ, тому довжина рівна 4.

length – це властивість

Люди з досвідом роботи в інших мовах випадково намагаються викликати властивість, додаючи круглі дужки: вони пишуть str.length() замість str.length. Це не спрацює.

Зверніть увагу, що str.length – це числове значення, а не функція, додавати дужки не потрібно. Не .length(), а .length.

Доступ до символів

Отримати символ, котрий займає позицію pos, можна за допомогою квадратних дужок: [pos], або викликати метод str.at(pos). Перший символ займає нульову позицію:

let str = `Привіт`;

// перший символ
alert( str[0] ); // П
alert( str.charAt(0) ); // П

// останній символ
alert( str[str.length - 1] ); // т
alert( str.at(-1) );

Як бачите, перевага методу .at(pos) полягає в тому, що він допускає від’ємну позицію. Якщо pos від’ємне число, тоді позиція відраховується з кінця рядка.

Отже, .at(-1) означає останній символ, а .at(-2) – передостанній, тощо.

Квадратні дужки завжди повертають undefined для від’ємних індексів, наприклад:

let str = `Привіт`;

alert( str[-2] ); // undefined
alert( str.at(-2) ); // і

Ми також можемо перебрати рядок посимвольно, використовуючи for..of:

for (let char of "Привіт") {
  alert(char); // П,р,и,в,і,т (char — спочатку "П", потім "р", потім "и" і так далі)
}

Рядки незмінні

В JavaScript рядки не можна змінювати. Змінити символ неможливо.

Спробуємо показати на прикладі:

let str = 'Ой';

str[0] = 'о'; // помилка
alert( str[0] ); // не працює

Можна створити новий рядок замість старого, записавши його в ту саму змінну.

Ось так:

let str = 'Ой';

str = 'о' + str[1]; // замінюємо рядок

alert( str ); // ой

В наступних розділах ми побачимо більше прикладів.

Зміна регістру

Методи toLowerCase() та toUpperCase() змінюють регістр символів:

alert( 'Інтерфейс'.toUpperCase() ); // ІНТЕРФЕЙС
alert( 'Інтерфейс'.toLowerCase() ); // інтерфейс

Або якщо ми хочемо перенести в нижній регістр конкретний символ:

alert( 'Interface'[0].toLowerCase() ); // 'і'

Пошук підрядка

Існує декілька способів для пошуку підрядка.

str.indexOf

Перший метод – str.indexOf(substr, pos).

Він шукає підрядок substr в рядку str, починаючи з позиції pos, і повертає позицію, де знаходиться збіг, або -1 якщо збігів не було знайдено.

Наприклад:

let str = 'Віджет з ідентифікатором';

alert( str.indexOf('Віджет') ); // 0, тому що 'Віджет' було знайдено на початку
alert( str.indexOf('віджет') ); // -1, збігів не знайдено, пошук чутливий до регістру

alert( str.indexOf("ід") ); // 1, підрядок "ід" знайдено на позиції 1 (..іджет з ідентифікатором)

Необовʼязковий другий параметр pos дозволяє нам почати пошук із заданої позиції.

Наприклад, перший збіг "ід" знаходиться на позиції 1. Щоб знайти наступний збіг, почнемо пошук з позиції 2:

let str = 'Віджет з ідентифікатором';

alert( str.indexOf('ід', 2) ) // 9

Щоб знайти усі збіги, нам потрібно запустити indexOf в циклі. Кожен новий виклик здійснюється з позицією після попереднього збігу:

let str = 'Хитрий, як лисиця, сильний, як Як';

let target = 'як'; // давайте знайдемо це

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos == -1) break;

  alert( `Знайдено тут: ${foundPos}` );
  pos = foundPos + 1; // продовжуємо з наступної позиції
}

Той самий алгоритм можна записати коротше:

let str = "Хитрий, як лисиця, сильний, як Як";
let target = "як";

let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
  alert( pos );
}
str.lastIndexOf(substr, position)

Також є схожий метод str.lastIndexOf(substr, position), що виконує пошук від кінця рядка до його початку.

У ньому будуть перераховані збіги в зворотному порядку.

Існує незручність з indexOf в умові if. Ми не можемо помістити його в if таким чином:

let str = "Віджет з ідентифікатором";

if (str.indexOf("Віджет")) {
    alert("Є співпадіння"); // не працює
}

В прикладі вище alert не відображається, оскільки str.indexOf("Віджет") повертає 0 (це означає, що він знайшов збіг у початковій позиції). Це правильно, але if вважає, що 0 – це false.

Тому нам потрібно робити перевірку на -1, як тут:

let str = "Віджет з ідентифікатором";

if (str.indexOf("Віджет") != -1) {
    alert("Є співпадіння"); // тепер працює!
}

includes, startsWith, endsWith

Сучасніший метод str.includes(substr, pos) повертає true/false в залежності від того чи є substr в рядку str.

Цей метод доцільно використовувати, коли потрібно перевірити чи є збіг, але не потрібна позиція:

alert( "Віджет з ідентифікатором".includes("Віджет") ); // true

alert( "Привіт".includes("Бувай") ); // false

Необовʼязковий другий аргумент pos – це позиція з якої почнеться пошук:

alert( "Віджет".includes("ід") ); // true
alert( "Віджет".includes("ід", 3) ); // false, починаючи з 3-го символа, підрядка "ід" немає

Відповідно, методи str.startsWith та str.endsWith перевіряють, чи починається і чи закінчується рядок певним підрядком.

alert( "Віджет".startsWith("Від") ); // true, "Віджет" починається з "Від"
alert( "Віджет".endsWith("жет") ); // true, "Віджет" закінчується підрядком "жет"

Отримання підрядка

В JavaScript є 3 метода для отримання підрядка: substring, substr та slice.

str.slice(start [, end])

Повертає частину рядка починаючи від start до (але не включно) end.

Наприклад:

let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', підрядок від 0 до 5 (5 не включно)
alert( str.slice(0, 1) ); // 's', від 0 до 1, але 1 не включно, тому лише символ на позиції 0

Якщо другий аргумент відсутній, тоді slice поверне символи до кінця рядка:

let str = "stringify";
alert( str.slice(2) ); // 'ringify', з позиції 2 і до кінця

Також для start/end можна задати відʼємне значення. Це означає, що позиція буде рахуватися з кінця рядка:

let str = "stringify";

// починаємо з 4-го символа справа, і закінчуємо на 1-му символі справа
alert( str.slice(-4, -1) ); // 'gif'
str.substring(start [, end])

Повертає частину рядка між start та end (не включаючи end)…

Цей метод майже такий самий що і slice, але він дозволяє задати start більше ніж end (у цьому випадку він просто міняє значення start і end місцями).

Наприклад:

let str = "stringify";

// для substring ці два приклади однакові
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"

// ...але не для slice:
alert( str.slice(2, 6) ); // "ring" (те саме)
alert( str.slice(6, 2) ); // "" (порожній рядок)

Відʼємні аргументи (на відміну від slice) не підтримуються, вони інтерпретуються як 0.

str.substr(start [, length])

Повертає частину рядка з позиції start, із заданою довжиною length.

На відміну від попередніх методів, цей дозволяє вказати довжину length замість кінцевої позиції:

let str = "stringify";
alert( str.substr(2, 4) ); // 'ring', починаючи з позиції 2 отримуємо 4 символа

Перший аргумент може бути відʼємним, щоб рахувати з кінця:

let str = "stringify";
alert( str.substr(-4, 2) ); // 'gi', починаючи з позиції 4 з кінця отримуєму 2 символа

Цей метод міститься в Annex B специфікації мови. Це означає, що лише рушії браузерного Javascript мають його підтримувати, і не рекомендується його використовувати. На практиці це підтримується всюди.

Давайте підсумуємо ці методи щоб не заплутатись:

Метод вибирає… відʼємні значення
slice(start, end) від start до end (не включаючи end) дозволяє відʼємні значення
substring(start, end) між start та end (не включаючи end) відʼємні значення інтерпретуються як 0
substr(start, length) length символів від start дозволяє відʼємні значення start
Який метод вибрати?

Усі вони можуть виконати задачу. Формально substr має незначний недолік: він описаний не в основній специфікації JavaScript, а в Annex B, який охоплює лише функції браузера, які існують переважно з історичних причин. Тому не браузерні середовища, можуть не підтримувати його. Але на практиці це працює всюди.

З двох інших варіантів slice дещо гнучкіший, він допускає від’ємні аргументи та коротший в записі.

Отже, достатньо запамʼятати лише slice з цих трьох методів.

Порівняння рядків

Як ми знаємо з розділу Оператори порівняння, рядки порівнюються символ за символом в алфавітному порядку.

Хоча, є деякі дивацтва.

  1. Літера в малому регістрі завжди більша за літеру у великому:

    alert( 'a' > 'Z' ); // true
  2. Літери з діакритичними знаками “не в порядку”:

    alert( 'Österreich' > 'Zealand' ); // true

    Це може призвести до дивних результатів, якщо ми відсортуємо ці назви країн. Зазвичай люди очікують, що Zealand буде після Österreich.

Щоб зрозуміти, що відбувається, давайте розглянемо внутрішнє представлення рядків у JavaScript закодованих за допомогою UTF-16. Тобто: кожен символ має відповідний числовий код…

Існують спеціальні методи, які дозволяють отримати символ по коду і навпаки.

str.codePointAt(pos)

Повертає десяткове число, що є кодом символу на позиції pos:

// літери в різному регістрі мають різні коди
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90
alert( "z".codePointAt(0) ); // 122
alert( "z".codePointAt(0).toString(16) ); // 7a (if we need a hexadecimal value)
String.fromCodePoint(code)

Створює символ за його кодом code

alert( String.fromCodePoint(90) ); // Z
alert( String.fromCodePoint(0x5a) ); // Z (ми також можемо використовувати шістнадцяткове значення як аргумент)

Тепер давайте подивимося на символи з кодами 65..220 (латинський алфавіт і трохи більше), створивши з них рядок:

let str = '';

for (let i = 65; i <= 220; i++) {
  str += String.fromCodePoint(i);
}
alert( str );
// Output:
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„
// ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ

Бачите? Спочатку вводяться великі символи, потім кілька спеціальних, потім символи нижнього регістру та Ö ближче до кінця виводу.

Тепер стає очевидним, чому a > Z.

Символи порівнюються за їх числовим кодом. Більший код означає, що символ більше. Код для a (97) більший за код для Z (90).

  • Усі малі літери йдуть після великих, оскільки їхні коди більші.
  • Деякі літери, як-от Ö, стоять окремо від основного алфавіту. Тут його код більший за будь-що від a до z.

Правильне порівняння

«Правильний» алгоритм порівняння рядків є складнішим, ніж може здатися, тому що для різних мов – різні алфавіти.

Отже, браузеру потрібно знати, яку мову використовувати для порівняння.

На щастя, усі сучасні браузери підтримують стандарт інтернаціоналізації ECMA-402.

Він забезпечує спеціальний метод для порівняння рядків різними мовами, дотримуючись їхніх правил.

Виклик str.localeCompare(str2) повертає ціле число, яке вказує, чи є str меншим, рівним чи більшим за str2 відповідно до правил мови:

  • Повертає відʼємне число, якщо str менше, ніж str2.
  • Повертає додатне число, якщо str більше, ніж str2.
  • Повертає 0, якщо вони рівні.

Наприклад:

alert( 'Österreich'.localeCompare('Zealand') ); // -1

Цей метод насправді має два додаткові аргументи, зазначені в документації, що дозволяє йому вказати мову (типово взяту з середовища, порядок букв залежить від мови) і встановити додаткові правила, як-от чутливість до регістру або чи слід розглядати різницю між "a" та "á".

Підсумки

  • Є 3 види лапок. Зворотні лапки дозволяють рядку охоплювати кілька ліній і застосовувати вбудовувані вирази ${…}.
  • Ми можемо використовувати спеціальні символи, такі як розрив рядка \n.
  • Щоб отримати символ, використовуйте: [] або метод at.
  • Щоб отримати підрядок, використовуйте: slice або substring.
  • Щоб перевести рядок у нижній/верхній регістри, використовуйте: toLowerCase/toUpperCase.
  • Щоб знайти підрядок, використовуйте: indexOf, або includes/startsWith/endsWith для простих перевірок.
  • Щоб порівняти рядки з урахуванням правил мови, використовуйте: localeCompare, інакше вони порівнюються за кодами символів.

Є кілька інших корисних методів у рядках:

  • str.trim() – видаляє (“обрізає”) пробіли з початку та кінця рядка.
  • str.repeat(n) – повторює рядок n разів.
  • …та багато іншого можна знайти в посібнику.

Рядки також мають методи пошуку/заміни регулярними виразами. Але це велика тема, тому пояснюється в окремому розділі Регулярні вирази.

Крім того, на даний момент важливо знати, що рядки базуються на кодуванні Unicode, і тому виникають проблеми з порівнянням, які ми описали вище. Більше про Unicode у розділі Юнікод, внутрішня будова рядків.

Завдання

важливість: 5

Напишіть функцію ucFirst(str), яка повертає рядок str з першим символом у верхньому регістрі, наприклад:

ucFirst("василь") == "Василь";

Відкрити пісочницю з тестами.

Ми не можемо “замінити” перший символ, оскільки рядки в JavaScript незмінні.

Але ми можемо створити новий рядок на основі існуючого, з першим символом у верхньому регістрі:

let newStr = str[0].toUpperCase() + str.slice(1);

Але є невелика проблема. Якщо str порожній рядок, то str[0] буде undefined, а оскільки undefined не має методу toUpperCase(), ми отримаємо помилку.

Найпростіший спосіб – додати перевірку на порожній рядок, наприклад ось так:

function ucFirst(str) {
  if (!str) return str;

  return str[0].toUpperCase() + str.slice(1);
}

alert( ucFirst("василь") ); // Василь

Відкрити рішення із тестами в пісочниці.

важливість: 5

Напишіть функцію checkSpam(str), яка повертає true, якщо str містить ‘viagra’ or ‘XXX’, інакше false.

Функція має бути нечутливою до регістру:

checkSpam('buy ViAgRA now') == true
checkSpam('free xxxxx') == true
checkSpam("innocent rabbit") == false

Відкрити пісочницю з тестами.

Щоб зробити пошук нечутливим до регістру, давайте переведемо рядок у нижній регістр, а потім здійснимо пошук:

function checkSpam(str) {
  let lowerStr = str.toLowerCase();

  return lowerStr.includes('viagra') || lowerStr.includes('xxx');
}

alert( checkSpam('buy ViAgRA now') );
alert( checkSpam('free xxxxx') );
alert( checkSpam("innocent rabbit") );

Відкрити рішення із тестами в пісочниці.

важливість: 5

Створіть функцію truncate(str, maxlength), яка перевіряє довжину str і, якщо вона перевищує maxlength – замінює кінець str символом трьох крапок "…", щоб його довжина була рівною maxlength.

Результатом функції повинен бути урізаний (якщо потребується) рядок.

Наприклад:

truncate("Що я хотів би розповісти на цю тему:", 20) = "Що я хотів би розпо…"

truncate("Всім привіт!", 20) = "Всім привіт!"

Відкрити пісочницю з тестами.

Максимальна довжина має бути maxlength, тому нам потрібно її трохи обрізати, щоб дати місце для символу трьох крапок.

Зауважте, що насправді існує один юнікодний символ для “трьох крапок”. Це не три послідовні крапки.

function truncate(str, maxlength) {
  return (str.length > maxlength) ?
    str.slice(0, maxlength - 1) + '…' : str;
}

Відкрити рішення із тестами в пісочниці.

важливість: 4

У нас є вартість у вигляді "$120". Тобто: спочатку йде знак долара, а потім число.

Створіть функцію extractCurrencyValue(str), яка витягне числове значення з такого рядка та поверне його.

Приклад:

alert( extractCurrencyValue('$120') === 120 ); // true

Відкрити пісочницю з тестами.

function extractCurrencyValue(str) {
  return +str.slice(1);
}

Відкрити рішення із тестами в пісочниці.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)