ES6引入了一種新型的字符串字面量語法,我們稱之為模板字符串(template strings)。除了使用反撇號字符 ` 代替普通字符串的引號 ' 或 " 外,它們看起來與普通字符串並無二致。在最簡單的情況下,它們與普通字符串的表現一致:
context.fillText(`Ceci n'est pas une chaîne.`, x, y);
但是我們並沒有說:“原來只是被反撇號括起來的普通字符串啊”。模板字符串名之有理,它為JavaScript提供了簡單的字符串插值功能,從此以后,你可以通過一種更加美觀、更加方便的方式向字符串中插值了。
模板字符串的使用方式成千上萬,但是最讓我會心一暖的是將其應用於毫不起眼的錯誤消息提示:
-
function authorize(user, action) {
-
if (!user.hasPrivilege(action)) {
-
throw new Error(
-
`用戶 ${user.name} 未被授權執行 ${action} 操作。`);
-
}
-
}
在這個示例中,${user.name}和${action}被稱為模板占位符,JavaScript將把user.name和action的值插入到最終生成的字符串中,例如:用戶jorendorff未被授權打冰球。(這是真的,我還沒有獲得冰球許可證。)
到目前為止,我們所了解到的僅僅是比 + 運算符更優雅的語法,下面是你可能期待的一些特性細節:
- 模板占位符中的代碼可以是任意JavaScript表達式,所以函數調用、算數運算等這些都可以作為占位符使用,你甚至可以在一個模板字符串中嵌套另一個,我稱之為模板套構(template inception)。
- 如果這兩個值都不是字符串,可以按照常規將其轉換為字符串。例如:如果action是一個對象,將會調用它的.toString()方法將其轉換為字符串值。
- 如果你需要在模板字符串中書寫反撇號,你必須使用反斜杠將其轉義:`\``等價於"`"。
- 同樣地,如果你需要在模板字符串中引入字符$和{。無論你要實現什么樣的目標,你都需要用反斜杠轉義每一個字符:`\$`和`\{`。
與普通字符串不同的是,模板字符串可以多行書寫:
-
$("#warning").html(`
-
<h1>小心!>/h1>
-
<p>未經授權打冰球可能受罰
-
將近${maxPenalty}分鍾。</p>
-
`);
模板字符串中所有的空格、新行、縮進,都會原樣輸出在生成的字符串中。
好啦,我說過要讓你們輕松掌握模板字符串,從現在起難度會加大,你可以到此為止,去喝一杯咖啡,慢慢消化之前的知識。真的,及時回頭不是一件令人感到羞愧的事情。Lopes Gonçalves曾經向我們證明過,船只不會被海妖碾壓,也不會從地球的邊緣墜落下去,他最終跨越了赤道,但是他有繼續探索整個南半球么?並沒有,他回家了,吃了一頓豐盛的午餐,你一定不排斥這樣的感覺。
反撇號的未來
當然,模板字符串也並非事事包攬:
- 它們不會為你自動轉義特殊字符,為了避免跨站腳本漏洞,你應當像拼接普通字符串時做的那樣對非置信數據進行特殊處理。
- 它們無法很好地與國際化庫(可以幫助你面向不同用戶提供不同的語言)相配合,模板字符串不會格式化特定語言的數字和日期,更別提同時使用不同語言的情況了。
- 它們不能替代模板引擎的地位,例如:Mustache、Nunjucks。
模板字符串沒有內建循環語法,所以你無法通過遍歷數組來構建類似HTML中的表格,甚至它連條件語句都不支持。你當然可以使用模板套構(template inception)的方法實現,但在我看來這方法略顯愚鈍啊。
不過,ES6為JS開發者和庫設計者提供了一個很好的衍生工具,你可以借助這一特性突破模板字符串的諸多限制,我們稱之為標簽模板(tagged templates)。
標簽模板的語法非常簡單,在模板字符串開始的反撇號前附加一個額外的標簽即可。我們的第一個示例將添加一個SaferHTML標簽,我們要用這個標簽來解決上述的第一個限制:自動轉義特殊字符。
請注意,ES6標准庫不提供類似SaferHTML功能,我們將在下面自己來實現這個功能。
-
var message =
-
SaferHTML`<p>${bonk.sender} 向你示好。</p>`;
這里用到的標簽是一個標識符SaferHTML;也可以使用屬性值作為標簽,例如:SaferHTML.escape;還可以是一個方法調用,例如:SaferHTML.escape({unicodeControlCharacters: false})。精確地說,任何ES6的成員表達式(MemberExpression)或調用表達式(CallExpression)都可作為標簽使用。
可以看出,無標簽模板字符串簡化了簡單字符串拼接,標簽模板則完全簡化了函數調用!
上面的代碼等效於:
-
var message =
-
SaferHTML(templateData, bonk.sender);
templateData是一個不可變數組,存儲着模板所有的字符串部分,由JS引擎為我們創建。因為占位符將標簽模板分割為兩個字符串的部分,所以這個數組內含兩個元素,形如Object.freeze(["<p>", " has sent you a bonk.</p>"]。
(事實上,templateData中還有一個屬性,在這篇文章中我們不會用到,但是它是標簽模板不可分割的一環:templateData.raw,它同樣是一個數組,存儲着標簽模板中所有的字符串部分,如果我們查看源碼將會發現,在這里是使用形如\n的轉義序列分行,而在templateData中則為真正的新行,標准標簽String.raw會用到這些原生字符串。)
如此一來,SaferHTML函數就可以有成千上萬種方法來解析字符串和占位符。
在繼續閱讀以前,可能你苦苦思索到底用SaferHTML來做什么,然后着手嘗試去實現它,歸根結底,它只是一個函數,你可以在Firefox的開發者控制台里測試你的成果。
以下是一種可行的方案(在gist中查看):
-
function SaferHTML(templateData) {
-
var s = templateData[0];
-
for (var i = 1; i < arguments.length; i++) {
-
var arg = String(arguments[i]);
-
-
// 轉義占位符中的特殊字符。
-
s += arg.replace(/&/g, "&")
-
.replace(/</g, "<")
-
.replace(/</g, ">");
-
-
// 不轉義模板中的特殊字符。
-
s += templateData[i];
-
}
-
return s;
-
}
通過這樣的定義,標簽模板SaferHTML`<p>${bonk.sender} 向你示好。</p>` 可能擴展為字符串 "<p>ES6<3er 向你示好。</p>"。即使一個惡意命名的用戶,例如“黑客Steve<script>alert('xss');</script>”,向其他用戶發送一條騷擾信息,無論如何這條信息都會被轉義為普通字符串,其他用戶不會受到潛在攻擊的威脅。
(順便一提,如果你感覺上述代碼中在函數內部使用參數對象的方式令你感到枯燥乏味,不妨期待下一篇大作,ES6中的另一個新特性一定會讓你眼前一亮!)
僅一個簡單的示例不足以說明標簽模板的靈活性,我們一起回顧下我們之前有關模板字符串限制的列表,看一下你還能做些什么不一樣的事情。
- 模板字符串不會自動轉義特殊字符。但是正如我們看到的那樣,通過標簽模板,你可以自己寫一個標簽函數來解決這個問題。
事實上,你可以做的比那更好。
站在安全角度來說,我實現的SaferHTML函數相當脆弱,你需要通過多種不同的方式將HTML不同部分的特殊字符轉義,SaferHTML就無法做到全部轉義。但是稍加努力,你就可以寫出一個更加智能的SaferHTML函數,它可以針對templateData中字符串中的HTML位進行解析,分析出哪一個占位符是純HTML;哪一個是元素內部屬性,需要轉義'和";哪一個是URL的query字符串,需要進行URL轉義而非HTML轉義,等等。智能SaferHTML函數可以將每個占位符都正確轉義。
HTML的解析速度很慢,這種方法聽起來是否略顯牽強?幸運的是,當模板重新求值的時候標簽模板的字符串部分是不改變的。SaferHTML可以緩存所有的解析結果,來加速后續的調用。(緩存可以按照ES6的另一個特性——WeakMap的形式進行存儲,我們將在未來的文章中繼續深入討論。)
- 模板字符串沒有內建的國際化特性,但是通過標簽,我們可以添加這些功能。Jack Hsu的一篇博客文章展示了具體的實現過程。我謹在此處拋磚引玉:
i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.` // => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.
注意觀察這個示例中的運行細節,name和amount都是JavaScript,進行正常插值處理,但是有一段與眾不同的代碼,:c(CAD),Jack將它放入了模板的字符串部分。JavaScript理應由JavaScript引擎進行處理,字符串部分由Jack的i18n標簽進行處理。使用者可以通過i18n的文檔了解到,:c(CAD)代表加拿大元的貨幣單位。
這就是標簽模板的大部分實際應用了。
- 模板字符串不能代替Mustache和Nunjucks,一部分原因是在模板字符串沒有內建的循環或條件語句語法。我們一起來看如何解決這個問題,如果JS不提供這個特性,我們就寫一個標簽來提供相應支持。
-
// 基於純粹虛構的模板語言
-
// ES6標簽模板。
-
var libraryHtml = hashTemplate`
-
<ul>
-
#for book in ${myBooks}
-
<li><i>#{book.title}</i> by #{book.author}</li>
-
#end
-
</ul>
-
`;
標簽模板帶來的靈活性遠不止於此,要記住,標簽函數的參數不會自動轉換為字符串,它們如返回值一樣,可以是任何值,標簽模板甚至不一定要是字符串!你可以用自定義的標簽來創建正則表達式、DOM樹、圖片、以promises為代表的整個異步過程、JS數據結構、GL着色器……
標簽模板以開放的姿態歡迎庫設計者們來創建強有力的領域特定語言。這些語言可能看起來不像JS,但是它們仍可以無縫嵌入到JS中並與JS的其它語言特性智能交互。我不知道這一特性將會帶領我們走向何方,但它蘊藏着無限的可能性,這令我感到異常興奮!
我什么時候可以開始使用這一特性?
在服務器端,io.js支持ES6的模板字符串。
在瀏覽器端,Firefox 34+支持模板字符串。它們由去年夏天的實習生項目組里的Guptha Rajagopal實現。模板字符串同樣在Chrome 41+中得以支持,但是IE和Safari都不支持。到目前為止,如果你想要在web端使用模板字符串的功能,你將需要Babel或Traceur協助你完成ES6到ES5的代碼轉譯,你也可以在TypeScript中立即使用這一特性。
等等——那么Markdown呢?
嗯?
哦…這是個好問題。
(這一章節與JavaScript無關,如果你不使用Markdown,可以跳過這一章。)
對於模板字符串而言,Markdown和JavaScript現在都使用`字符來表示一些特殊的事物。事實上,在Markdown中,反撇號用來分割在內聯文本中間的代碼片段。
這會帶來許多問題!如果你在Markdown中寫這樣的文檔:
To display a message, write `alert(`hello world!`)`.
它將這樣顯示:
To display a message, write alert(hello world!).
請注意,輸出文本中的反撇號消失了。Markdown將所有的四個反撇號解釋為代碼分隔符並用HTML標簽將其替換掉。
為了避免這樣的情況發生,我們要借助Markdown中的一個鮮為人知的特性,你可以使用多行反撇號作為代碼分隔符,就像這樣:
To display a message, write ``alert(`hello world!`)``.
在這個Gist有具體代碼細節,它由Markdown寫成,所以你可以直接查看源代碼。