ES6 系列之模板字串

冴羽發表於2018-05-30

基礎用法

let message = `Hello World`;
console.log(message);
複製程式碼

如果你碰巧要在字串中使用反撇號,你可以使用反斜槓轉義:

let message = `Hello \` World`;
console.log(message);
複製程式碼

值得一提的是,在模板字串中,空格、縮排、換行都會被保留:

let message = `
	<ul>
		<li>1</li>
		<li>2</li>
	</ul>
`;
console.log(message);
複製程式碼

string

注意,列印的結果中第一行是一個換行,你可以使用 trim 函式消除換行:

let message = `
	<ul>
		<li>1</li>
		<li>2</li>
	</ul>
`.trim();
console.log(message);
複製程式碼

string

嵌入變數

模板字串支援嵌入變數,只需要將變數名寫在 ${} 之中,其實不止變數,任意的 JavaScript 表示式都是可以的:

let x = 1, y = 2;
let message = `<ul><li>${x}</li><li>${x + y}</li></ul>`;
console.log(message); // <ul><li>1</li><li>3</li></ul>
複製程式碼

值得一提的是,模板字串支援巢狀:

let arr = [{value: 1}, {value: 2}];
let message = `
	<ul>
		${arr.map((item) => {
			return `
				<li>${item.value}</li>
			`
		})}
	</ul>
`;
console.log(message);
複製程式碼

列印結果如下:

string

注意,在 li 標籤中間多了一個逗號,這是因為當大括號中的值不是字串時,會將其轉為字串,比如一個陣列 [1, 2, 3] 就會被轉為 1,2,3,逗號就是這樣產生的。

如果你要消除這個逗號,你可以先 join 一下:

let arr = [{value: 1}, {value: 2}];
let message = `
	<ul>
		${arr.map((item) => {
			return `
				<li>${item.value}</li>
			`
		}).join('')}
	</ul>
`;
console.log(message);
複製程式碼

列印結果如下:

string

標籤模板

模板標籤是一個非常重要的能力,模板字串可以緊跟在一個函式名後面,該函式將被呼叫來處理這個模板字串,舉個例子:

let x = 'Hi', y = 'Kevin';
var res = message`${x}, I am ${y}`;
console.log(res);
複製程式碼

我們可以自定義 message 函式來處理返回的字串:

// literals 文字
// 注意在這個例子中 literals 的第一個元素和最後一個元素都是空字串
function message(literals, value1, value2) {
	console.log(literals); // [ "", ", I am ", "" ]
	console.log(value1); // Hi
	console.log(value2); // Kevin
}
複製程式碼

我們利用這些引數將其拼合回去:

function message(literals, ...values) {
	let result = '';

	for (let i = 0; i < values.length; i++) {
		result += literals[i];
		result += values[i];
	}

	result += literals[literals.length - 1];

	return result;
}
複製程式碼

你也可以這樣寫:

function message(literals, ...values) {
	let result = literals.reduce((prev, next, i) => {
	    let value = values[i - 1];
	    return prev + value + next;
	});

	return result;
}
複製程式碼

學著拼合回去是一件非常重要的事情,因為我們經過各種處理,最終都還是要拼回去的……

oneLine

講完了基礎,我們可以來看一些實際的需求:

let message = `
	Hi,
	Daisy!
	I am
	Kevin.
`;
複製程式碼

出於可讀性或者其他原因,我希望書寫的時候是換行的,但是最終輸出的字元是在一行,這就需要藉助模板標籤來實現了,我們嘗試寫一個這樣的函式:

// oneLine 第一版
function oneLine(template, ...expressions) {
    let result = template.reduce((prev, next, i) => {
        let expression = expressions[i - 1];
        return prev + expression + next;
    });

    result = result.replace(/(\s+)/g, " ");
    result = result.trim();

    return result;
}
複製程式碼

實現原理很簡單,拼合回去然後將多個空白符如換行符、空格等替換成一個空格。

使用如下:

let message = oneLine `
    Hi,
    Daisy!
    I am
    Kevin.
`;
console.log(message); // Hi, Daisy! I am Kevin.
複製程式碼

不過你再用下去就會發現一個問題,如果字元間就包括多個空格呢?舉個例子:

let message = oneLine`
  Preserve eg sentences.  Double
  spaces within input lines.
`;
複製程式碼

如果使用這種匹配方式,sentences.Double 之間的兩個空格也會被替換成一個空格。

我們可以再優化一下,我們想要的效果是將每行前面的多個空格替換成一個空格,其實應該匹配的是換行符以及換行符後面的多個空格,然後將其替換成一個空格,我們可以將正則改成:

result = result.replace(/(\n\s*)/g, " ");
複製程式碼

就可以正確的匹配程式碼。最終的程式碼如下:

// oneLine 第二版
function oneLine(template, ...expressions) {
    let result = template.reduce((prev, next, i) => {
        let expression = expressions[i - 1];
        return prev + expression + next;
    });

    result = result.replace(/(\n\s*)/g, " ");
    result = result.trim();

    return result;
}
複製程式碼

stripIndents

假設有這樣一段 HTML:

let html = `
	<span>1<span>
	<span>2<span>
		<span>3<span>
`;
複製程式碼

為了保持可讀性,我希望最終輸入的樣式為:

<span>1<span>
<span>2<span>
<span>3<span>
複製程式碼

其實就是匹配每行前面的空格,然後將其替換為空字串。

// stripIndents 第一版
function stripIndents(template, ...expressions) {
    let result = template.reduce((prev, next, i) => {
        let expression = expressions[i - 1];
        return prev + expression + next;
    });


    result = result.replace(/\n[^\S\n]*/g, '\n');
    result = result.trim();

    return result;
}
複製程式碼

最難的或許就是這個正規表示式了:

result = result.replace(/\n[^\S\n]*/g, '\n');
複製程式碼

\S 表示匹配一個非空白字元

[^\S\n] 表示匹配非空白字元換行符之外的字元,其實也就是空白字元去除換行符

\n[^\S\n]* 表示匹配換行符以及換行符後的多個不包含換行符的空白字元

replace(/\n[^\S\n]*/g, '\n') 表示將一個換行符以及換行符後的多個不包含換行符的空白字元替換成一個換行符,其實也就是將換行符後面的空白字元消掉的意思

其實吧,不用寫的這麼麻煩,我們還可以這樣寫:

result = result.replace(/^[^\S\n]+/gm, '');
複製程式碼

看似簡單了一點,之所以能這樣寫,是因為匹配模式的緣故,你會發現,這次除了匹配全域性之外,這次我們還匹配了多行,m 標誌用於指定多行輸入字串時應該被視為多個行,而且如果使用 m 標誌,^ 和 $ 匹配的開始或結束是輸入字串中的每一行,而不是整個字串的開始或結束。

[^\S\n] 表示匹配空白字元去除換行符

^[^\S\n]+ 表示匹配以去除換行符的空白字元為開頭的一個或者多個字元

result.replace(/^[^\S\n]+/gm, '') 表示將每行開頭一個或多個去除換行符的空白字元替換成空字串,也同樣達到了目的。

最終的程式碼如下:

// stripIndents 第二版
function stripIndents(template, ...expressions) {
    let result = template.reduce((prev, next, i) => {
        let expression = expressions[i - 1];
        return prev + expression + next;
    });


    result = result.replace(/^[^\S\n]+/gm, '');
    result = result.trim();

    return result;
}
複製程式碼

stripIndent

注意,這次的 stripIndent 相比上面一節的標題少了一個字母 s,而我們想要實現的功能是:

let html = `
	<ul>
		<li>1</li>
		<li>2</li>
		<li>3</li>
	<ul>
`;
複製程式碼

string

其實也就是去除第一行的換行以及每一行的部分縮排。

這個實現就稍微麻煩了一點,因為我們要計算出每一行到底要去除多少個空白字元。

實現的思路如下:

  1. 使用 match 函式,匹配每一行的空白字元,得到一個包含每一行空白字元的陣列
  2. 陣列遍歷比較,得到最小的空白字元長度
  3. 構建一個正規表示式,然後每一行都替換掉最小長度的空白字元

實現的程式碼如下:

let html = `
	<ul>
		<li>1</li>
		<li>2</li>
		<li>3</li>
	<ul>
`;


function stripIndent(template, ...expressions) {
    let result = template.reduce((prev, next, i) => {
        let expression = expressions[i - 1];
        return prev + expression + next;
    });

    const match = result.match(/^[^\S\n]*(?=\S)/gm);
    console.log(match); // Array [ "    ", "        ", "        ", "        ", "    " ]

    const indent = match && Math.min(...match.map(el => el.length));
    console.log(indent); // 4

    if (indent) {
        const regexp = new RegExp(`^.{${indent}}`, 'gm');
        console.log(regexp); // /^.{4}/gm

        result =  result.replace(regexp, '');
    }

    result = result.trim();

    return result;
}
複製程式碼

值得一提的是,我們一般會以為正則中 . 表示匹配任意字元,其實是匹配除換行符之外的任何單個字元。

最終精簡的程式碼如下:

function stripIndent(template, ...expressions) {
    let result = template.reduce((prev, next, i) => {
        let expression = expressions[i - 1];
        return prev + expression + next;
    });

    const match = result.match(/^[^\S\n]*(?=\S)/gm);
    const indent = match && Math.min(...match.map(el => el.length));

    if (indent) {
        const regexp = new RegExp(`^.{${indent}}`, 'gm');
        result =  result.replace(regexp, '');
    }

    result = result.trim();

    return result;
}
複製程式碼

includeArrays

前面我們講到為了避免 ${} 表示式中返回一個陣列,自動轉換會導致多個逗號的問題,需要每次都將陣列最後再 join('') 一下,再看一遍例子:

let arr = [{value: 1}, {value: 2}];
let message = `
	<ul>
		${arr.map((item) => {
			return `
				<li>${item.value}</li>
			`
		}).join('')}
	</ul>
`;
console.log(message);
複製程式碼

利用標籤模板,我們可以輕鬆的解決這個問題:

function includeArrays(template, ...expressions) {
    let result = template.reduce((prev, next, i) => {

        let expression = expressions[i - 1];

        if (Array.isArray(expression)) {
            expression = expression.join('');
        }

        return prev + expression + next;
    });

    result = result.trim();

    return result;
}
複製程式碼

最後

你會發現以上這些函式拼合的部分都是重複的,我們完全可以將其封裝在一起,根據不同的配置實現不能的功能。如果你想在專案中使用這些函式,可以自己封裝一個或者直接使用 common-tags

ES6 系列

ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog。

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。

相關文章