Глава 2
Strings и Регулярни изрази

Strings е може би един от най-важните типове данни в програмирането. Те са в почти всеки език за програмиране от по-високо ниво, който е в състояние да работи с тях ефективно и са от основно значение за програмистите да създават полезни програми. Чрез разширяване, регулярните изрази са важни, защото дават допълнителна мощност на програмистите, която да упражнят върху strings. С тези факти в ума, създателите на ECMAScript 6 подобряват strings и регулярните изрази, чрез добавяне на нови възможности и дълго липсваща функционалност. Тази глава обхваща тези два вида промени.

По-добра Unicode поддръжка

Преди ECMAScript 6 в JavaScript, strings се въртят около 16-битово character (характер) кодиране (UTF-16). Всяка 16-битова последователност е кодови единици( code unit) представляващи характер. Всички string свойства и методи, като length свойство и charAt() метод, се основават на тези 16-битови кодови единици. Разбира се, 16-бита се използват за да бъдат достатъчни да съдържат всеки характер. Това вече не е вярно благодарение на разширения набор от характери въведени с Unicode.

UTF-16 кодови точки

Ограничаване дължината на характера до 16 бита не е възможно, заявиха от Unicode с цел на осигуряване на глобален уникален идентификатор за всеки характер в света. Тези глобални уникални идентификатори, наречени code points, са просто номера започващи от 0. Можете да мислите за кодовите точки, като характер кодове, където всеки номер представлява характер. Кодирането на характерите превежда кодовите точки в кодови единици, които са вътрешно последователни. За UTF-16, кодовите точки могат да бъдат съставени от много кодови единици.

Първите 2 16 кодови точки в UTF-16 са представени, като единични 16-битови кодови единици. Този диапазон се нарича Basic Multilingual Plane (BMP). Всичко след това се счита за допълнителни нива, където кодовите точки не могат да бъдат представени с 16-бита. UTF-16 решава този проблем чрез въвеждане на сурогатни двойки, при които една кодова точка е представена от две 16-битови кодови единици. Това означава, че всеки един характер в string може да бъде или една кодова единица за BMP характер, което прави общо 16 бита, или две единици за допълнително ниво, което прави общо 32 бита.

В ECMAScript 5, всички string операции работят с 16-битови кодови единици, което означава, че може да получите неочаквани резултати от UTF-16 кодирани strings съдържащи сурогатни двойки, като в този пример:

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 string операциите я третират, като два 16-битови характера. Това означава:

  • Дължината на текста е 2, а трябва да бъде 1.
  • Регулярния израз се опитва да съответства на един характер, но не успява защото си мисли, че има два характера.
  • Метода charAt() не е в състояние да върне влиден характер, защото набора от 16 бита не съответства на печатния характер.

Метода charCodeAt() не може просто да определи характера правилно. Той връща съответния 16-битов номер за всяка кодова единица, но това е най-близко, което може да получите за реалната стойност на text в ECMAScript 5.

ECMAScript 6, от друга страна, налага UTF-16 string кодиране за справяне с подобни проблеми. Стандартизиране на string операциите основаващи се на такова кодиране на характерите в JavaScript, може да поддържа функционалност проектирана да работи специално със сурогатни двойки. В останалата част от този раздел се разглеждат няколко ключови примера на тази функционалност.

Метода codePointAt()

Един метод в ECMAScript 6, който добавя изцяло подкрепа за UTF-16 е codePointAt() метода, който извлича Unicode кодова точка от maps за дадена позиция в string. Този метод приема позицията на код единица, а не позицията на характера и връща цялочислена стойност, както оператора 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() метода, освен ако ако не оперира с non-BMP характери. Първият характер в text е non-BMP и следователно се състои от две кодови единици, което означава, че length свойството е 3, а не 2. Методът 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, докато String.fromCodePoint() произвежда единичен характер от дадена кодова точка. Например:

console.log(String.fromCodePoint(134071));  // "𠮷"				
			

Мислете за String.fromCodePoint(), като по-пълна версия на String.fromCharCode() метода. И двата дават един и същи резултат за характерите на BMP. Има само една разлика, когато премине кодовите точки за характерите извън BMP.

Метода normalize()

Друг интересен аспект от Unicode е, че различните характери могат да се считат за еквивалентни за целите на сортиране или други операции за сравняване. Има два начина да се определят тези отношения. Първия е, канонична еквивалентност, което означава две последователности от кодови точки се считат за взаимозаменяеми във всяко отношение. Така например, комбинация от два характера може да бъде канонично еквивалентна на един характер. Вторият е съвместимост. Две съвместими последователности от кодови точки изглеждат по различен начин, но могат да се използват взаимозаменяеми в определени ситуации.

Благодарение на тези взаимоотношения, два strings, представляващи основно същия текст може да съдържат различни последователности от кодови точки. Например, характера “æ” и двойния характер string “ae” може да се използват, като взаимозаменяеми, но те не са еквивалентни ако се нормализират по някакъв начин.

ECMAScript 6 поддържа Unicode форми за нормализация на даден string, като метода normalize(). Този метод евентуално приема един единствен string параметър, посочвайки Unicode формите за нормализация, които да приложи:

  • Normalization Form Canonical Composition ("NFC"), който е по подразбиране
  • Normalization Form Canonical Decomposition ("NFD")
  • Normalization Form Compatibility Composition ("NFKC")
  • Normalization Form Compatibility Decomposition ("NFKD")

Извън обхвата на тази книга е да се обясни разликата между тези четири форми. Само имайте в предвид, че при сравняване на strings, двата strings трябва да бъдат нормализирани към една и съща форма. Например:

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;
    }
});	
			

Този код превръща strings във values в нормализирана форма, така че array да бъде подреден по подходящ начин. Може също да се сортира оригиналния array, като се извика 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;
    }
});		
			

За последно, най-важното нещо, което трябва да се отбележи за този код е, че и двата и първия и втория са нормализирани по един и същи начин. Тези примери са използвани по подразбиране, 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 strings. Стандарта също така добавя и два полезни синтактични елемента.

Регулярен израз и u Flag

Може да постигнете много string операции използвайки регулярни изрази. Но не забравяйте, че регулярните изрази приемат 16-битов код, където всеки от тях е един характер. За да се справи с този проблем, ECMAScript 6 дефинира u flag за регулярни изрази, което е съкращение на Unicode.

u Flag в действие

Когато регулярен израз има u flag set, той превключва режима на работа на характерите, а не на кодовите единици. Това означава, че регулярния израз вече не трябва да се бърка от сурогатните двойки в strings и трябва да се държи, както се очаква. Например да разгледаме този код:

var text = "𠮷";
console.log(text.length);           // 2
console.log(/^.$/.test(text));      // false
console.log(/^.$/u.test(text));     // true			
				

Регулярния израз /^.$/ съвпада с всеки входящ string с един характер. Когато се използва без u flag, този регулярен израз съвпада с кодовите единици и така Японския характер (който е представен от две кодови единици) не съответства на регулярния израз. Когато се използва u flag, регулярния израз сравнява характери вместо кодови единици и така Японския характер съвпада.

Преброяване на кодови точки

За съжаление, ECMAScript 6 не може да определи, колко кодови точки съдържа един string, но може да използвате u flag с регулярен израз за да разберете това:

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 за харатери на празни и не празни пространства (използвайки [\s\S] за да гарантира, че моделът съвпада с новите редове) с помощта на регулярен израз, който се прилага глобално с Unicode подръжка. result съдържа array от съвпадения, дори да има най-малко едно съвпадение, така че дължината на array е броя на кодовите точки в string. В Unicode, и двата strings "abc" и "𠮷bc" имат три характера, така че дължината на array е три.

worning
Въпреки, че този подход работи, той не е много бърз, особено ако се прилага за дълги strings. Може да използвате string итератор (обсъден в Глава 8). Като цяло се опитайте да сведете до минимум броенето на кодови точки, когато е възможно.

Определяне на поддръжка за u Flag

Тъй като u flag е промяна на синтаксиса, опитите да се използва в JavaScript машини, които не са съвместими с ECMAScript 6 хвърля синтактична грешка. Най-сигурният начин да се определи дали u flag се поддържа е да се подкрепи с функция като тази:

function hasRegExpU(){  
    try {
        var pattern = new RegExp (".", "u");
        return true;
    }catch (ex) {
        return false;
    }
}	
				

Тази функция използва RegExp конструктор за да приеме u flag, като аргумент. Този синтаксис е валиден дори в по-стари машини на JavaScript и конструктора ще хвърли грешка ако u не се поддържа.

info
Ако вашия код все пак трябва да работи в по-стари машини на JavaScript, винаги използвайте RegExp конструктора при използването на u flag. Това ще попречи на синтактичните грешки и ще ви позволи оптимално използване на u flag без да абортирате изпълнението.

Други string промени

JavaScript strings винаги са изоставали спрямо сходни характеристики на други езици. Това беше само до ECMAScript 5, където тези strings най-накрая получиха trim() метод и ECMAScript 6 продължава разширяването на капацитета на JavaScript да прави разбор на strings с нова функционалност.

Методи за идентифициране на Substrings

Програмистите в JavaScript използват indexOf() метода, за да определят какви strings има в други strings. ECMAScript 6 въвежда следните три метода, които са предназначени да правят това:

  • Метода includes() - връща истина, ако даден текст е намерен някъде в рамките на string. И връща false, ако не е намерен.
  • Метода startsWith() - връща истина, ако даден текст е намерен в началото на string. И връща false ако не е намерен.
  • Метода endsWith() - връща истина, ако даден текст е намерен в края на string. И връща false, ако не е намерен.

Тези методи приемат два аргумента: текст за търсене и не задължителен индекс от къде да започне търсенето. Когато се предостави втори аргумент, includes() и startsWith() започват съвпадението от този индекс, докато endsWith() започва съвпадението от дължината на string минус втория аргумент; когато втория аргумент е пропуснат, includes() и startsWith() търсят в началото на string, а endsWith() започва от края. Смисъла е, че втория аргумент намалява размера на string при търсене. Ето няколко примера, които показват тези три метода в действие:

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
				
			

Първите три извиквания не включват втори параметър, така че те ще търсят в целия string, ако е необходимо. Последните три извиквания проверяват само част от string. Извикването на msg.startsWith("o", 4) започва съвпадението от индекс 4 на msg string, който е “o” в “Hello”. Извикването на msg.endsWith("o", 8) започва съвпадението от индекс 4, тъй като 8 се изважда от дължината на string (12). Извикването на msg.includes("o", 8) започва съвпадението от индекс 8, който е “r” в “world”.

worning
Методите startsWith(), endsWith() и includes() хвърлят грешка, ако им се подаде регулярен израз вместо string. Това контрастира с indexOf() и lastIndexOf(), които едновременно превръщат регулярен израз в аргумент на string и след това търсят този string.

Метода repeat()

ECMAScript 6 също добавя метода repeat() за strings, който приема повторението на string, като аргумент. Той връща нов string, съдържащ оригиналния string повторен определен брой пъти. Например:

console.log("x".repeat(3));         // "xxx"
console.log("hello".repeat(2));     // "hellohello"
console.log("abc".repeat(4));       // "abcabcabcabc"
			

Този метод е удобна функция, която може да бъде полезна при манипулиране на текст. Това е особено полезно в кодово форматиране на utilities, които трябва да създадат нива за отстояние, като тези:

// indent използва определен брой места
var indent = " ".repeat(4),   
    indentLevel = 0;
// когато се увеличи indent
var newIndent = indent.repeat(++indentLevel);	
			

Първия repeat() създава string от четири позиции, а indentLevel държи променливата на нивата. След това може просто да се извика repeat() с нарастващ indentLevel за промяна на броя на местата.

ECMAScript 6 също прави някои промени за функционалността на регулярните изрази, които не се вписват в определена категория. Следващия раздел подчертава няколко.

Други промени на регулярните изрази

Регулярните изрази са важна част от работата със strings в JavaScript, подобно на много части от езика, те не са се променили много през последните версии. ECMAScript 6, обаче, прави няколко подобрения заедно с актуализациите на strings.

Регулярни изрази с y Flag

ECMAScript 6 стандартизира y flag след, като бе въведен във Firefox, като разширение на свойство за регулярни изрази. Ефекта на y flag в регулярния израз е да търси sticky свойство и казва на търсачката да започне съвпадение на знаци в string от позиция посочена от регулярния израз на 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 няма flags, този в globalPattern използва g flag, а този в stickyPattern използва y flag. Първата тройка на console.log() извиква трите регулярни израза, които трябва да върнат "hello1 " с интервал на края.

След това, lastIndex свойството се променя на 1 и на трите модела, което означава, че регулярният израз започва да търси съвпадения от втория характер. Регулярният израз без flags напълно игнорира промяната на lastIndex и ще търси съвпадения на "hello1 " без инциденти. Регулярния израза със g flag търси съвпадения на "hello2 " защото търси напред след втория характер на string, който е ("e"). Регулярния израз sticky не съвпада с нищо, започвайки от втория характер, така че stickyResult е null.

sticky flag запазва индекса на следващия характер след последното съвпадение в lastIndex всеки път, когато се извършва операция. Ако резултатите от операцията нямат съвпадение, lastIndex ще върне 0. global flag се държи по същия начин, както е показано тук:

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.

Има два по-фини детайла за sticky flag, които трябва да запомним:

  1. Свойството lastIndex се зачита, само след извикване на методи, които съществуват в обекта на регулярните изрази, като exec() и test() методите. Подаване на регулярен израз към string методи, като match(), няма да доведе до sticky поведение.
  2. При използване на ^ характер за съвпадение с начало на string, регулярните изрази sticky съответстват само с началото на string (или от началото на линията за многоредов модел). Докато lastIndex е 0, ^ прави регулярния израз sticky да не се различава от non-sticky. Ако lastIndex не съответства на началото на единичен string или на началото на линията на многоредов модел, регулярния израз sticky никога няма да съвпадне.

Както и при другите flags на регулярните изрази, може да се открие съвпадение с y, с използването на свойства. В този случай, ще се провери за sticky свойството, както следва:

var pattern = /hello\d/y;
 console.log(pattern.sticky);    // true		
			

Свойството sticky е вярно, ако присъства sticky flag и свойството е false ако го няма. Свойството sticky се отчита въз основа на присъствието на flag и не може да се промени в кода.

Подобно на u flag, y flag е промяна в синтаксиса, така че това ще предизвика синтактична грешка при по-стари машини на JavaScript. Можете да използвате следния код за откриване на поддръжка:

function hasRegExpY() {   
    try {  
        var pattern = new RegExp(".", "y");
        return true;
    } catch (ex) {
        return false;
    }
}		
			

Точно, както в u проверката, този израз връща false ако не е в състояние да създаде регулярен израз с y flag. В едно последно сходство със u, ако трябва да се използва y в кода на по-стари JavaScript машини, не забравяйте да използвате RegExp конструктора при дефинирането на регулярни изрази, за да се избегне синтактична грешка.

Копиране на регулярни изрази

В ECMAScript 5, може да се копират регулярни изрази, чрез подаването им в RegExp конструктора, като това:

var re1 = /ab/i,
    re2 = new RegExp(re1);	
			

Променливата re2 е просто копие на променливата re1. Но ако се предостави втори аргумент на RegExp конструктора, който определя flags за регулярния израз, този код няма да работи, както в този пример:

var re1 = /ab/i,   
    // хвърля грешка в ES5, но е наред в ES6
    re2 = new RegExp(re1, "g");	
			

Ако се изпълни този код в средата на ECMAScript 5, ще получите съобщение за грешка, че вторият аргумент не може да се използва, когато първият аргумент е регулярен израз. ECMAScript 6 променя това поведение, като разрешава втори аргумент и той има предимство пред всички flags, които присъстват в първия аргумент. Например:

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 flag, докато re2 има само глобален g flag. Конструктора RegExp копира модела от re1 и замества i flag със g flag. Без втория аргумент, re2 ще има същия flags, като re1.

Свойството flags

Заедно с добавянето на нов flag и промяната на начина, по който може да се работи с flags, ECMAScript 6 добавя свойство свързано с тях. В ECMAScript 5, бихме могли да получим текста на регулярен израз, чрез използване на source свойство, за да получим flag string, ще трябва да се направи разбор на изхода на toString() метода, както е показано по долу:

function getFlags(re) { 
    var text = re.toString();
    return text.substring(
        text.lastIndexOf("/") + 1, text.length);
}

// toString() is "/ab/g"
var re = /ab/g;
console.log(getFlags(re));          // "g"
			

Това превръща регулярния израз в string и след това се връща за да намери последния характер след /. Този характер е flags.

ECMAScript 6 прави извличането на flags по-лесно, чрез добавяне на flags свойство към source свойството. И двете свойства имат прототип accessor свойство, което ги прави само за четене. Свойството flags прави проверката на регулярни изрази по-лесна за отстраняване на грешки и наследени цели.

Едно закъсняло допълнение към ECMAScript 6, за flags свойството е връщането на string представяне на всякакви flags, приложени към регулярния израз. Например:

var re = /ab/g;
console.log(re.source);     // "ab"
console.log(re.flags);      // "g"
			

Това извлича всички flags на re и ги отпечатва на конзолата с далеч по-малко редове код, от колкото toString() техниката може. Използването на source и flags заедно ни позволява да извлечем парчета от регулярния израз, от които имаме нужда без директен разбор на израза в string.

Промените на strings и регулярните изрази обхванати в тази глава определено са мощни, но ECMAScript 6 увеличава силата си върху strings по много по-голям начин. Тя въвежда template literals при правенето на strings, което ги прави по-гъвкави.

Template Literals

Strings в JavaScript винаги са имали ограничена функционалност в сравнение със strings в другите езици. Така например, до ECMAScript 6 за strings липсваха методите обхванати в тази глава и съединяването на string беше токова просто, колкото е възможно. За да даде възможност на програмистите да решават по-сложни проблеми, template literals на ECMAScript 6 предоставят синтаксис за създаване на домейн-специфични езици (DSLs) за по-сигурен начин на работа със съдържанието, от наличните по-ранни решения в ECMAScript 5. (DSL е програмен език, предназначен за конкретна цел, за разлика от езици с общо предназначение, като JavaScript). В уикипедия ECMAScript предлага следното описание на template literal strawman:

Тази схема разширява синтаксиса на ECMAScript със syntactic sugar за да даде възможност на библиотеките да предоставят на DSLs възможност, лесно да произвежда заявки и манипулира съдържанието от други езици, които са имунизирани или устойчиви на атаките на XSS, SQL Injection и т.н.

В действителност, обаче, template literals са отговор на ECMAScript 6 за следните признаци, които липсваха на JavaScript през целия път в ECMAScript 5:

  • Multiline strings - официална концепция за многоредови strings.
  • Basic string formatting -способност да се замени част от string със стойности, които се съдържат в променливи.
  • HTML escaping - способност да се трансформира string, така че безопасно да се вмъкне в HTML.

Вместо да се опитват да добавят повече функционалност към вече съществуващата за strings в JavaScript, template literals представляват един изцяло нов подход към решаването на тези проблеми.

Basic Syntax

Простите, template literals действат, като редовни strings оградени от backticks (`) (апостроф) вместо с двойни или единични кавички. Например, помислете за следното:

let message = `Hello world!`;
console.log(message);               // "Hello world!"
console.log(typeof message);        // "string"
console.log(message.length);        // 12
			

Този код показва, че променливата message съдържа нормален JavaScript string. Синтаксиса на template literal се използва за създаване на string стойност, която след това се възлага на message променливата.

Ако искате да използвате апостроф в string, трябва да (избягате) escape с обратно наклонена черта (\), както в този вариант на message променливата:

let message = `\`Hello\` world!`;
console.log(message);               // "`Hello` world!"
console.log(typeof message);        // "string"
console.log(message.length);        // 14			
			

Няма нужда от escape на двойни или единични кавички вътре в template literals.

Multiline Strings

JavaScript програмистите са търсили начин за създаване на няколко реда strings, както в първата версия на езика. Но когато се използват единични или двойни кавички, strings трябва задължително да бъдат само на един ред.

Pre-ECMAScript 6 Workarounds

Благодарение на дълго-годишни синтактични бъгове, JavaScript не разполага със заобикаляне на проблема. Може да създадете няколко реда strings ако има обратно наклонена черта (\) преди всеки нов ред. Ето един пример

var message = "Multiline \
string";
console.log(message);       // "Multiline string"				
				

В message string няма нови редове, които да се представят при отпечатване на конзолата, защото наклонената черта се третира, като продължение, а не като нов ред. За да се покаже новия ред в продукцията, ще трябва да се включи ръчно:

var message = "Multiline \n\
string";
console.log(message);       // "Multiline
                            //  string"
			

Това трябва да отпечата Multiline String на две отделни линии на всички основни JavaScript машини, но това поведение често се определя като грешка и много програмисти препоръчват да се избягва.

Други опити преди ECMAScript 6 за създаване на няколко реда strings обикновено разчитат на arrays или на конкатенация на strings, като например:

var message = [  
    "Multiline ", 
    "string"
].join("\n");
let message = "Multiline \n" +
    "string";
				

При всички от начините, JavaScript програмистите са работили около липсата на няколко реда strings, като още нещо, което да се желае.

Multiline Strings - лесния начин

ECMAScript 6 template literals правят няколко реда strings лесно, защото няма специален синтаксис. Просто включвате нов ред където искате и го показвате в резултата. Например:

let message = `Multiline
string`;
console.log(message);           // "Multiline
                                //  string"
console.log(message.length);    // 16	
			

Всички празни пространства вътре в апострофите са част от string, така че внимавайте с отстоянието. Например:

let message = `Multiline
               string`;
console.log(message);           // "Multiline
                                //                 string"
console.log(message.length);    // 31
			

В този код, всички празни места преди втората линия на template literal се считат, като част от string. Ако направата на текст с правилни отстояния е важно за вас, помислете първо да не оставите нищо на първия ред от multiline template и след това отстоянията, както следва:

let html = `
<div>
    <h1>Title</h1>
</div>`.trim();		
			

Този код започва от първата линия, но няма никакъв текст до втората линия. Таговете на HTML за отстъп изглеждат правилно и после trim() метода се извиква за да премахне първия празен ред.

Ако предпочитате, можете да използвате \n в template literal за да посочи къде трябва да се вмъкне нов ред:

let message = `Multiline\nstring`;
 console.log(message);           // "Multiline
                                 //string"
console.log(message.length);     // 16			
			

Осъществяване на замествания

До този момент, template literals може да изглеждат, като красиви версии на нормални JavaScript strings. Истинската разлика е в template literal заместванията. Заместванията позволяват да вградим всеки валиден израз в JavaScript, вътре в template literal и изходния резултат, като част от string.

Заместванията са разделени с отваряне ${ и затваряне }, като могат да имат всеки JavaScript израз вътре. Най-простото заместване позволява да вградите локални променливи директно в резултата на string, като това:

let name = "Nicholas",
    message = `Hello, ${name}.`;
console.log(message);       // "Hello, Nicholas."
			

Замяната ${name} има достъп до локалната променлива name и я вмъква в message string. Променливата message държи в резултата тази замяна.

info
Template literal има достъп до всяка достъпна променлива в обхвата, в който се определя. Всеки опит да се използва необявена променлива в template literal хвърля грешка и в двата режима strict и non-strict modes.

Тъй като всички замествания са в JavaScript изрази, можем да заместим повече от прости имена на променливи. Можем лесно да вградим изчисления, извиквания на функции и още.. . Например:

let count = 10,
    price = 0.25,
    message = `${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message);       // "10 items cost $2.50."
			

Този код извършва изчисление, като част от template literal. Променливите count и price се умножават, за да получим резултат, който се форматира с два знака след десетичната запетая използвайки .toFixed(). Знакът за долар $ преди второто заместване се извежда, както е , защото не е последван от фигурни скоби.

Template literals са също JavaScript изрази, което означава, че може да се постави template literal вътре в друг template literal, както в този пример:

let name = "Nicholas",
    message = `Hello, ${`my name is ${ name }`}.`;
console.log(message);        // "Hello, my name is Nicholas."	
			

В този пример, вторият template literal е вътре в първия. След първия ${, започва другия template literal. Втория ${ показва началото на вградения израз на вътрешния template literal. Тази експресия е на променливата name, която се вмъква в резултата.

Tagged Templates

Досега видяхме, как template literals могат да създават по няколко реда strings и да вмъкваме стойности в string без конкатенация. Но истинската сила на template literals идва от tagged templates.    template tag извършва трансформация на template literal и връща крайната string стойност. Този tag е посочен в началото на template, точно преди първия ` характер, както е показано тук:

let message = tag`Hello world`;				
			

В този пример, tag е template tag , който се прилага към `Hello world` template literal.

Определяне на Tags

Tag е проста функция, която се извиква с данните на template literal.    tag получава данните за template literal, като отделни парчета и трябва да комбинира парчетата за да създаде резултата. Първият аргумент е array, съдържащ strings, който се тълкува от JavaScript. Всеки следващ аргумент се тълкува от стойността на замяната.

Tag функциите обикновено се определят с помощта rest аргументи, които следва да се занимават с данните по-лесно:

function tag(literals, ...substitutions) {
     // return a string
}
			

За да разберем по-добре това, което се подава на таговете, имайте в предвид следното:

let count = 10,
    price = 0.25,
    message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;	
			

Ако имате функция наречена passthru(), тази функция ще получи три аргумента. Първо, тя ще получи literals array, съдържащ следните елементи:

  • празен string преди заместването ("")
  • string след първото заместване и преди второто (" items cost $")
  • string след второто заместване (".")

Следващия аргумент ще бъде 10, който интерпретира стойността на count променливата. Това ще бъде първия елемент в substitutions array. Последния аргумент ще бъде "2.50", който е тълкуваната стойност на (count * price).toFixed(2) и вторият елемент на substitutions array.

Имайте в предвид, че първият елемент в literals е празен string. Това гарантира, че literals[0] е винаги в началото на string, точно както literals[literals.length - 1] е винаги края на string. Винаги има едно по-малко в заместването на literal, което означава, че израза substitutions.length === literals.length - 1 винаги е верен.

Използвайки този модел на literals и substitutions array, те могат да се преплитат за създаване на резултат в string. Първият елемент в literals е на първо място, първият елемент в substitutions е на следващо място, и т.н., докато string се запълни. Като пример, можем да имитираме поведението по подразбиране на template literal с редуващите се стойности на тези два arrays:

function passthru(literals, ...substitutions) {
    let result = "";
    // пускаме цикъл само за броя на замените
    for (let i = 0; i < substitutions.length; i++) {
        result += literals[i];
        result += substitutions[i];
    }
    // добавяне на  последния literal
    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 tag, който изпълнява същата трансформация, както поведението по подразбиране на template literal. Единственият трик е използването на substitutions.length за цикъла, вместо literals.length за да се избегне случайно подминаване на края на substitutions array. Това работи, защото връзката между literals и substitutions е добре дефинирана в ECMAScript 6.

info
Стойностите, които се съдържат в substitutions не са непременно strings. Ако израза оценява номера, както в предишния пример, тогава се подава цифрова стойност вътре. Определянето, как тази стойност да се изведе в резултата е част от работата на tags.

Използване на необработени стойности в Template Literals

Template tags също имат достъп до информацията на необработен string, което означава най-вече достъп до escapes на характера, преди трансформацията им в еквивалентни характери. Най-простия начин да се работи с необработени string стойности е използването на вградения String.raw() tag. Например:

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 характери). Извличането на информация от необработен string, дава възможност за по-сложна обработка, когато е необходимо.

Информацията от необработен string също се подава на template tags. Първият аргумент в tag функцията е array с възможност за допълнително свойство, наречено raw. Свойството raw е array съдържащ необработен еквивалент на всяка literal стойност. Например, стойността на literals[0] винаги е еквивалент на literals.raw[0], която съдържа необработена string информация. Знаейки това, можем да имитираме String.raw() използвайки следния код:

function raw(literals, ...substitutions) {
    let result = "";
    // пускаме цикъл само за броя на замените
    for (let i = 0; i < substitutions.length; i++) {
        result += literals.raw[i];   // използваме необработена 
        стойност
        result += substitutions[i];
    }
    // добавяне на последния literal
    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 за изходния резултат на string. Това означава, че всеки escapes характер, включително и Unicode кодовата точка escapes, трябва да бъде върнат в необработен вид. Raw strings са полезни, когато искаме изходния string да съдържа код в който трябва да се включи escaping на характера (например, ако искаме да се генерира документация на някакъв код, можем да искаме в продукцията да се вижда действителния код).