在 ES6 中引入了一種新的字串字面量 — 模板字串,除了使用反引號 (`) 表示,它們看上去和普通的字串沒有什麼區別。在最簡單的情況下,他們就是普通的字串:
1 |
context.fillText(`Ceci n'est pas une chaîne.`, x, y); |
之所以被稱為模板字串,是因為模板字串為 JS 引入了簡單的字串插值特性,也就是說,可以方便優雅地將 JS 的值插入到字串中。
很多地方可以用到模板字串,看下面這個不起眼的錯誤提示訊息:
1 2 3 4 5 6 |
function authorize(user, action) { if (!user.hasPrivilege(action)) { throw new Error( `User ${user.name} is not authorized to do ${action}.`); } } |
上面程式碼中,${user.name}
和 ${action}
被稱為模板佔位符,JavaScript 將把 user.name
和 action
的值分別插到對應的位置上,然後生成像這樣 “User jorendorff is not authorized to do hockey.” 的字串。
現在,我們看到了一個比 +
運算子更優雅的語法,下面是一些你期待的特性:
- 模板佔位符可以是任何 JavaScript 表示式,所以函式呼叫和四則運算等都是合法的。(甚至你還可以在一個模板字串中巢狀另一個模板字串。)
- 如果一個值不是字串,它將被轉換為字串。例如,如果
action
是一個物件,那麼該物件的.toString()
將被呼叫,來將其轉換為字串。 - 如果你想在模板字串中使用反引號,你需要使用反斜槓
\
將其轉義。 - 同樣地,如果想在模板字串中輸出
${
,也需要使用反斜槓將其轉義:\${
或$\{
。 - 模板字串可以跨越多行:
1 2 3 4 5 |
$("#warning").html(` <h1>Watch out!</h1> <p>Unauthorized hockeying can result in penalties of up to ${maxPenalty} minutes.</p> `); |
- 模板字串中所有的空格、換行和縮排,都將被原樣輸出到結果字串中。
下面我們來看看模板字串做不到的事情:
- 不會自動轉義特殊字元,為了避免跨站指令碼漏洞,你還是需要小心對待不可信的資料,這一點上與普通字串一樣。
- 不能與國際化庫配合使用,不處理特殊語言格式的數字、日期等。
- 不是模板引擎(比如 Mustache 或 Nunjucks)的替代品。模板字串沒有處理迴圈的語法 — 不能通過一個陣列構建出一個表格(table)。
為了解決這些限制,ES6 為開發者和庫設計者提供了另一種模板字串 — 標籤模板。
標籤模板的語法很簡單,只需要在開始的反引號前引入一個標籤。看第一個例子:SaferHTML
,我們要使用這個標籤模板來解決上述的第一個限制:自動轉義特殊字元。
需要注意的是,SaferHTML
方法並不是 ES6 標準庫提供的,我們需要自己來實現:
1 2 |
var message = SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`; |
這裡的 SaferHTML
標籤是單個識別符號,標籤也可以是屬性,比如 SaferHTML.escape
,甚至還可以是方法呼叫:SaferHTML.escape({unicodeControlCharacters: false})
。準確地說,任何 ES6 的成員表示式或呼叫表示式都可以作為標籤。
可以看出,模板字串僅僅是字串連線的語法糖,而標籤模板確是一個完全不同的東西:函式呼叫。
所以,上面程式碼等價於:
1 2 |
var message = SaferHTML(templateData, bonk.sender); |
其中 templateData
是一個不可變的字串陣列,由 JS 引擎基於源模板字串生成,這裡的陣列含有兩個元素,因為模板字串被佔位符分隔後含有兩個字串,因此,templateData
將是這樣: Object.freeze(["<p>", " has sent you a bonk.</p>"]
(事實上,templateData
上還有另一個屬性:templateData.raw
,本文並深入不討論該屬性。該屬性的值也是一個陣列,包含了標籤模板中所有的字串部分,但字串中包含了轉義序列,看上去更像原始碼中的字串,比如 \n
。ES6 的內建標籤 String.raw
將使用這些字串。)
這就使得 SaferHTML
方法可以隨意解析這兩個字串,存在 N 中替換方式。
在繼續閱讀錢,你可能在苦苦思索如何實現 SaferHTML
方法。
下面是一種實現(gist):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function SaferHTML(templateData) { var s = templateData[0]; for (var i = 1; i < arguments.length; i++) { var arg = String(arguments[i]); // Escape special characters in the substitution. s += arg.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">"); // Don't escape special characters in the template. s += templateData[i]; } return s; } |
有了上面的方法,即使使用一個惡意的使用者名稱,使用者也是安全的。
一個簡單的例子並不足以說明標籤模板的靈活性,讓我們重溫一下上面列舉的模板字串的限制,看看我們還可以做些什麼。
- 模板字串不會自動轉義特殊字元,但是我們可以通過標籤模板來解決這個問題,事實上我們還可以將
SaferHTML
這個方法寫的更好。從安全形度來看,這個SaferHTML
非常脆弱。在 HTML 中,不同的地方需要用不同的方式去轉義,SaferHTML
並沒有做到。稍加思考,我們就可以實現一個更加靈活的SaferHTML
方法,能夠將templateData
中的任何一個 HTML 轉義,知道哪個佔位符是純 HTML;哪個是元素的屬性,從而需要對'
和"
轉義;哪個是 URL 的 query 字串,從而需要用 URL 的 escaping 方法,而不是 HTML 的 escaping;等等。這似乎有些牽強,因為 HTML 轉義效率比較低。辛運是的,標籤模板的字串是保持不變的,SaferHTML
可以快取已經轉義過的字串,從而提高效率。 - 模板字串並沒有內建的國際化特性,但通過標籤模板,我們可以新增該特性。Jack Hsu 的文章詳細介紹了實現過程,看下面例子:
1 2 |
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
很好理解,將被 JS 引擎替換為對應的字串,但是還有一個沒有見過的佔位符::c(CAD)
,這將被 i18n
標籤處理,從 i18n
的文件可知::c(CAD)
表示 amount
是加拿大美元貨幣值。
- 模板字串不能替代 Mustache 和 Nunjucks 這類别範本引擎,部分原因在於模板字串不支援迴圈和條件語句。我們可以編寫一個標籤來實現這類功能:
1 2 3 4 5 6 7 8 9 |
// Purely hypothetical template language based on // ES6 tagged templates. var libraryHtml = hashTemplate` <ul> #for book in ${myBooks} <li><i>#{book.title}</i> by #{book.author}</li> #end </ul> `; |
靈活性還不止於此,需要注意的是,標籤函式的引數不會自動轉換為字串,引數可以是任何型別,返回值也一樣。標籤模板甚至可以不需要字串,你可以使用自定義標籤來建立正規表示式、DOM 樹、圖片、代表整個非同步程式的 Promise、JS 資料結構、GL 著色器…
標籤模板允許庫設計者建立強大的領域特定語言。這些語言可能看上去並不像 JS,但他們可以無縫嵌入到 JS 中,並且可以與語言的其餘部分進行互動。順便說一下,我還沒有在其他語言中見過類似的特性,我不知道這個特性講給我們帶來些什麼,但各種可能性還是非常令人興奮的。