ES6(ES2015)爲 JavaScript 引入了許多新特性,其中與字符串處理相關的一個新特性——模板字面量,提供了多行字符串、字符串模板的功能,相信不少人已經在使用了。模板字面量的基本使用很簡單,但大多數開發者仍是僅僅把它當成字符串拼接的語法糖來使用的,實際上它的能力比這要強大得多哦。誇張一點地說,這多是 ES6 這麼多特性中,最容易被低估的特性了。Here is why。javascript
模板字面量在 ES2015 規範中叫作 Template Literals,在規範文檔更早的版本中叫 Template Strings,因此咱們見過的中文文檔不少也有把它寫成 模板字符串 的,有時爲表述方便也非正式地簡稱爲 ES6 模板。php
在 ES6 以前的 JavaScript,字符串做爲基本類型,其在代碼中的表示方法只有將字符串用引號符(單引號 ' 或 雙引號 ")包裹起來,ES6 模板字面量(下文簡稱 ES6 模板)則使用反撇號符(`)包裹做爲字符串表示法。html
兩個反撇號之間的常規字符串保持原樣,如:前端
`hello world` === "hello world" // --> true
`hello "world"` === 'hello "world"' // --> true
`hello 'world'` === "hello 'world'" // --> true
`\`` // --> "`" // --> true
複製代碼
換行符也只是一個字符,所以模板字面量也天然就支持多行字符 :java
console.log(`TODO LIST: * one * two `)
// TODO LIST:
// * one
// * two
複製代碼
兩個反撇號之間以 ${expression}
格式包含任意 JavaScript 表達式,該 expression 表達式的值會轉換爲字符串,與表達式先後的字符串拼接。expression 展開爲字符串時,使用的是 expression.toString()
函數。git
const name = "Alice"
const a = 1
const b = 2
const fruits = ['apple', 'orange', 'banana']
const now = new Date()
console.log(`Hello ${name}`)
console.log(`1 + 2 = ${a + b}`)
console.log(`INFO: ${now}`)
console.log(`Remember to bring: ${ fruits.join(', ') }.`)
console.log(`1 < 2 ${ 1 < 2 ? '✔︎' : '✘'}`)
// Hello Alice
// 1 + 2 = 3
// INFO: Sun May 13 2018 22:28:26 GMT+0800 (中國標準時間)
// Remember to bring: apple, orange, banana.
// 1 < 2 ✔︎
複製代碼
正由於 expression 能夠是 任意 JavaScript 表達式 ,而任意一個模板字符串自己也是一個表達式,因此模板中的 expression 能夠嵌套另外一個模板。es6
const fruits = ['apple', 'orange', 'banana']
const quantities = [4, 5, 6]
console.log(`I got ${ fruits .map((fruit, index) => `${quantities[index]} ${fruit}s`) .join(', ') }`)
// I got 4 apples, 5 oranges, 6 bananas
複製代碼
從目前的幾個示例,咱們已經掌握了 ES6 模板的基礎功能,但已足夠見識到它的本領。經過它咱們能夠很輕易地進行代碼重構,讓字符串拼接的代碼再也不充滿亂七八糟的單引號、雙引號、+
操做符還有反斜槓 \
,變得清爽不少。github
因而咱們很天然地想到,在實際應用中字符串拼接最複雜的場景——HTML 模板上,若是採用 ES6 模板是否能夠勝任呢?傳統上咱們採用專門的模板引擎作這件事情,不妨將 ES6 模板與模版引擎作對比。咱們選擇 lodash
的 _.template
模板引擎,這個引擎雖不像 mustache、pug 大而全,但提供的功能已足夠完備,咱們就從它的幾個核心特性和場景爲例,展開對比。正則表達式
第 1,基本的字符串插值。_.template
使用 <%= expression %>
做爲模板插值分隔符,expression 的值將會按原始輸出,與 ES6 模板相同。因此在這一個特性上,ES6 模板是徹底勝任的。shell
lodash
const compiled = _.template('hello <%= user %>!')
console.log(compiled({ user: 'fred' }))
// hello fred
複製代碼
ES6 模板
const greeting = data => `hello ${data.user}`
console.log(greeting({ user: 'fred' }))
// hello fred
複製代碼
第 2,字符串轉義輸出,這是 HTML 模板引擎防範 XSS 的標配功能,其原理就是要將插值表達式的值中包含的 <
、>
這種可能用於 XSS 攻擊的字符轉義爲 HTML Entity。要讓 lodash 輸出轉義後的插值表達式,使用 <%- expression %>
語法便可;而若是要使用 ES6 模板方案,就要靠本身實現一個單獨的函數調用了。在下面的示例代碼中,就定義了簡單的 escapeHTML 函數,在模板字符串中調用該函數對字符串進行轉義。
在這個特性上,ES6 能夠的確能夠作到相同的效果,但代價是要本身定義轉義函數並在表達式中調用,使用起來不如模板引擎封裝好的接口方便。
lodash
const compiled = _.template('<b><%- value %></b>')
複製代碼
ES6 模板
const entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
}
const escapeHTML = string => String(string).replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
const greeting = data => `hello ${escapeHTML(data.user)}`
console.log(greeting({ user: '<script>alert(0)</script>'}));
// hello <script>alert(0)</script>
複製代碼
第 3,模板內嵌 JavaScript 語句,也就是模板引擎支持經過在模板中執行 JavaScript 語句,生成 HTML。說白了其原理與世界上最好的語言 php 的 idea 是同樣的。在 lodash 模板中,使用 <% statement %>
就能夠執行 JS 語句,一個最典型的使用場景是使用 for 循環在模版中迭代數組內容。
但 ES6 模板中的佔位符 ${}
只支持插入表達式,因此要在其中直接執行 for 循環這樣的 JavaScript 語句是不行的。可是不要緊,一樣的輸出結果咱們用一些簡單的技巧同樣能夠搞定,例如對數組的處理,咱們只要善用數組的 map
、 reduce
、filter
,令表達式的結果符合咱們須要便可。
lodash
/* 使用 for 循環語句,迭代數組內容並輸出 */
const compiled = _.template('<ul><% for (var i = 0; i < users.length; i++) { %><li><%= list[i] %></li><% } %></ul>')
console.log(compiled({ users: ['fred', 'barney'] }))
// <ul><li>fred</li><li>barney</li></ul>
複製代碼
ES6 模板
const listRenderer = data => `<ul>${data.users.map(user => `<li>${user}</li>`).join('')}</ul>`
console.log(listRenderer({ users: ['fred', 'barney']}))
// <ul><li>fred</li><li>barney</li></ul>
複製代碼
在以上這 3 個示例場景上,咱們發現 lodash 模板能作的事,ES6 模板也均可以作到,那是否是能夠拋棄模板引擎了呢?
的確,若是在開發中只是使用以上這些基本的模板引擎功能,咱們能夠確實能夠直接使用 ES6 模板作替換,API 更輕更簡潔,還節省了額外引入一個模板庫的成本。
但若是咱們使用的是 pug、artTemplate、Handlebars 這一類大而全的模板引擎,使用 ES6 模板替換就不必定明智了。尤爲是這些模板引擎在服務器端場景下的模板緩存、管道輸出、模板文件模塊化管理等特性,ES6 模板自己都不具有。而且模板引擎做爲獨立的庫,API 的封裝和擴展性都比 ES6 模板中只能插入表達式要好。
截至目前介紹的特性,咱們能夠從上面 HTML 模板字符串轉義和數組迭代輸出兩個例子的代碼發現這樣一個事實:
要用 ES6 模板實現複雜一點的字符串處理邏輯,要依賴咱們寫函數來實現。
幸運的是,除了在模板的插值表達式裏想辦法調用各類字符串轉換的函數以外,ES6 還提供了更加優雅且更容易複用的方案——帶標籤的模板字面量(tagged template literals,如下簡稱標籤模板)。
標籤模板的語法很簡單,就是在模板字符串的起始反撇號前加上一個標籤。這個標籤固然不是隨便寫的,它必須是一個可調用的函數或方法名。加了標籤以後的模板,其輸出值的計算過程就再也不是默認的處理邏輯了,咱們如下面這一行代碼爲例解釋。
const message = l10n`I bought a ${brand} watch on ${date}, it cost me ${price}.`
複製代碼
在這個例子裏,l10n
就是標籤名,反撇號之間的內容是模板內容,這一行語句將模板表達式的值賦給 message
,具體的處理過程爲:
JS 引擎會先把模板內容用佔位符分割,把分割獲得的字符串存在數組 strings
中(如下代碼僅用來演示原理)
const strings = "I bought a ${brand} watch on ${date}, it cost me ${price}".split(/\$\{[^}]+\}/)
// ["I bought a ", " watch on ", ", it cost me ", "."]
複製代碼
而後再將模板內容裏的佔位符表達式取出來,依次存在另外一個數組 rest
中
const rest = [brand, date, price]
複製代碼
執行 i18n(strings, ...rest)
函數,即調用 l10n
,並傳入兩部分參數,第一部分是 strings
做爲第一個參數,第二部分是將 rest
展開做爲餘下參數。
const message = l10n(strings, ...rest)
複製代碼
所以,若是將以上單步分解合併在一塊兒,就是這樣的等價形式:
const message = l10n(["I bought a ", " watch on ", ", it cost me ", "."], brand, date, price)
複製代碼
也就是說當咱們給模板前面加上 l10n
這個標籤時,其實是在調用 l10n
函數,並以上面這種方式傳入調用參數。 l10n
函數能夠交給咱們自定義,從而讓上面這一行代碼👆輸出咱們想要的字符串。例如咱們若是想讓其中的日期和價格用本地字符串顯示,就能夠這樣實現:
const l10n = (strings, ...rest) => {
return strings.reduce((total, current, idx) => {
const arg = rest[idx]
let insertValue = ''
if (typeof arg === 'number') {
insertValue = `¥${arg}`
} else if (arg instanceof Date) {
insertValue = arg.toLocaleDateString('zh-CN')
} else if (arg !== undefined) {
insertValue = arg
}
return total + current + insertValue
}, '');
}
const brand = 'G-Shock'
const date = new Date()
const price = 1000
l10n`I bought a ${brand} watch on ${date}, it cost me ${price}.`
// I bought a G-Shock watch on 2018/5/16, it cost me ¥1000.
複製代碼
這裏的 l10n
就是個簡陋的傻瓜式的本地化模板標籤,它支持把模板內容裏的數字當成金額加上人民幣符號,把日期轉換爲 zh-CN
地區格式的 2018/5/16
字符串。
乍一看沒什麼大不了的,但設想一下相同的效果用沒有標籤的模板要怎樣實現呢?咱們須要在模板以內的日期表達式和價格數字表達式上調用相應的轉換函數,即 ${date.toLocaleDateString('zh-CN')}
和 ${ '¥' + price}
。一次調用差異不大,兩次、三次調用的狀況下,帶標籤的模板明顯勝出。不只符合 DRY(Don't Repeat Yourself)原則,也可讓模板代碼更加簡潔易讀。
帶標籤的模板字面量創建在很是簡單的原理上——經過本身的函數,自定義模板的輸出值。
tag`template literals`
複製代碼
而 ES6 規範沒有對這裏可使用的 tag
函數作任何限制,意味着 任何函數 均可以做爲標籤加到模板前面,也就意味着這個函數:
async
函數(支持函數內 await
異步語句並返回 Promise)因此只要你願意,A:你能夠把 任意 JavaScript 語句 放到標籤函數裏面去;B:你可讓標籤函數返回 任意值 做爲模板輸出。有了如此強大的擴展能力,也難怪一開始 ES6 標準中對模板規範的命名是 Template Strings,後來正式命名卻改爲了 Template Literals,由於它的能力已經超越了模板字符串,去追求詩和遠方了。
固然在實際的使用中仍是應該保持理智,不能手裏拿着錘子看什麼都像釘子。而如下這 5 種應用場景,卻是能夠算是真正的釘子。
String.raw
保留原始字符串String.raw
是 ES2015 規範新增的 String
對象的靜態成員方法,但一般並不做爲函數直接調用,而是做爲語言標準自帶的模板字面量標籤使用,用來保留模板內容的原始值。
其做用與 Python 語言中,字符串表達式的引號前加 r
前綴效果相似。JavaScript 須要這樣的特性,只是恰好用 String.raw
實現了它。
var a = String.raw`\n\n`
var b === '\n\n'
// a === b -> false
// a.length === 4 -> true
// b.length === 2 -> true
複製代碼
String.raw
標籤在某些場景下能夠幫咱們節省不少繁瑣的工做。其中一個典型的場景就是,實際開發中咱們常遇到在不一樣編程語言之間經過 JSON 文本傳輸協議數據的狀況,若是遇到包含轉義字符和 "
(JSON 屬性需用引號)的文本內容,很容易因細節處理不當致使 JSON 解析出錯。
而我本人遇到過這樣一個真實案例,就是由 Android 終端向 JavaScript 傳輸 JSON 數據時,有一條數據中用戶的暱稱包含了 "
字符。JavaScript 收到的 JSON 字符串文本爲:
{"nickname":"槍鍋&[{鍋}]\"鍋\":鍋","foo":"bar"}
複製代碼
但這裏若是直接將內容用引號賦值給 JSON.parse('{"nickname":"槍鍋&[{鍋}]\"鍋\":鍋","foo":"bar"}')
就會遇到 SyntaxError: Unexpected token 鍋 in JSON at position 22
的錯誤,由於單引號中的 \
被做爲轉義字符,未保留在 input
的值中。要獲得正確的 JSON 對象,使用 String.raw 處理便可:
JSON.parse(String.raw`{"nickname":"槍鍋&[{鍋}]\"鍋\":鍋","foo":"bar"}`)
// { nickname: '槍鍋&[{鍋}]"鍋":鍋', foo: 'bar' }
複製代碼
前面講到 ES6 模板與模板引擎的對比時,提到模板引擎經過手動 escapeHTML 模板轉義不安全的字符的問題。如今咱們瞭解了標籤模板以後,能夠將外部定義的 escapeHTML 邏輯直接放到標籤函數中,這樣就不須要在模板中每個插入表達式前,都調用 escapeHTML 函數了。
const safeHTML = (strings, ...rest) => {
const entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
}
const escapeHTML = string => String(string).replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
return strings.reduce((total, current, idx) => {
const value = rest[idx] || ''
return total + current + escapeHTML(value)
}, '');
}
const evilText = '<script>alert(document.cookie)</script>'
safeHTML`${evilText}`
// "<script>alert(document.cookie)</script>"
複製代碼
咱們這裏實現的 safeHTML 做爲 demo 用,並不保證生產環境完備。
你必定想到了,像 HTML 模板這樣的經常使用模板標籤必定有現成的 npm 庫了。沒錯, common-tags
庫就是咱們想要的這個庫了。common-tags
是一個小而精的模板標籤庫,被包括 Angular, Slack, Ember 等在內的不少大型項目所依賴。它包含了十幾個經常使用的用於字符串模板處理的函數,例如 html
, safeHTml
等,讓咱們能夠偷懶少造一些輪子。
const safeHtml = require('common-tags/lib/safeHtml')
const evilText = '<script>alert(document.cookie)</script>'
safeHtml`<div>${evilText}</div>`
複製代碼
common-tags
也提供了擴展 API,生成可讓咱們更輕鬆地實現自定義的標籤。
common-tags
庫裏包含的 html
和 safeHtm
模板標籤,依然是返回字符串的函數。這意味着若是咱們將它用在瀏覽器端獲得了模板值 output
字符串,依然要進一步使用 element.innerHTML = output
才能夠將模板內容渲染到 DOM 元素,顯示到界面。
不過既然模板字面量的標籤函數能夠返回任意值,咱們不妨直接再進一步將 element.innerHTML = output
語句也放到標籤函數中,並將 element 做爲返回值。這樣一來咱們就能夠直接用模板字面量的值做爲真實的 DOM Element ,而且直接用 DOM API 操做其返回值了。
不出意外,這個想法固然也有人已想到了,@choojs/nanohtml
就是作這件事的。choojs
是一個還很年輕的前端框架(又來),不過咱們今天不討論它,只看它所包含的 nanohtml
。nanohtml 的基本思路能夠用這樣的 Hello World 來演示:
var nanohtml = require('nanohtml')
var element = nanohtml`<div>Hello World!</div>`
document.body.appendChild(element)
複製代碼
除了像代碼所示返回 DOM Element,nanohtml 也支持在模板中的插值表達式中插入任意的標準 DOM Element,插入的元素會根據模板 markup 渲染,做爲返回值 element 的子元素。
var nanohtml = require('nanohtml')
var h3 = document.createElement('h3')
h3.textContent = 'Hello World'
var element = nanohtml`<div>${h3}</div>`
document.body.appendChild(element)
複製代碼
結合 ES2017 的 async
await
特性,咱們還能夠定義一個 async 類型的標籤函數,並在函數中調用異步操做以達到。
例如上面實現的傻瓜式 l10n
標籤,咱們可讓它更傻瓜一些,把模板最後拼接出的英文句子,翻譯爲本地語言以後再輸出。而翻譯的過程,就能夠經過異步調用第三方的翻譯 API 例如 Google Translate API 等來實現。因而咱們就有這樣的僞代碼:
const l10n = async (strings, ...rest) => {
return strings.reduce((total, current, idx) => {
const arg = rest[idx]
let insertValue = ''
if (typeof arg === 'number') {
insertValue = `¥${arg}`
} else if (arg instanceof Date) {
insertValue = arg.toLocaleDateString('zh-CN')
} else if (arg !== undefined) {
insertValue = arg
}
const line = total + current + insertValue
const language = navigator.language // "zh-CN"
// 僞裝 TRANSLATE_SERVICE 是一個可用的翻譯服務
const translated = await TRANSLATE_SERVICE.translate(line, { language })
return translated
}, '')
}
l10n`I bought a ${'G-Shock'} watch on ${new Date()}, it cost me ${1000}`.then(console.log)
// "我在 2018/5/16 買了一個 G-Shock 手錶,花了我 1000 元"
複製代碼
固然在不支持 async、await 也不支持 generator 生成器的環境下,使用 Promise 封裝異步調用,使標籤函數返回 Promise 實例,也能夠實現異步操做。
const tag = (strings, ...rest) => {
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'Hello World')
})
}
tag`Anything`.then(console.log)
// 1s 後 console 將輸出:
// "Hello World"
複製代碼
DSL(領域專用語言)是一個挺唬人的概念,顧名思義所謂 DSL 是指未解決某一特定領域的問題推出的語言。DSL 有按某種條件約束的規則——語法,故稱得上是語言。一般 DSL 用於編程領域,經過計算機程序處理 DSL,有些功能強大的 DSL 語言也會被認爲是 mini 編程語言。
典型的 DSL 有:
合理使用 ES6 標籤模板,可讓 JavaScript 對內嵌 DSL 的處理更加簡潔。嚴格來講,咱們上面用例 2 和用例 3 對中的 html
類模板標籤,就算是 DSL 的範疇了。下面是一些典型的案例:
注意:如下模板標籤僅做描述,須要自行實現
例如咱們能夠用標籤實現一個 DOM 選擇器,形如:
var elements = query`.${className}` // the new way
複製代碼
雖然第一眼看上去只是另外一個 jQuery 選擇器,但這種調用方式自然支持咱們在模板之中嵌入任意的插值表達式,在 API 的 expressive 這個特性上有提高。並且 query 函數由咱們自由實現,意味着能夠自由地在 query 函數中加入 selector 值規範化校驗,返回值規範化等額外功能。
在 Node.js 環境下,咱們還能夠實現一個 sh
標籤用於描述 shell 腳本的調用:
var proc = sh`ps aux | grep ${pid}`
複製代碼
看到這行語句咱們下意識地會想到,這裏的 API 會調用 shell 執行模板拼接出來的命令,而 proc 應該是該命令執行的結果或輸出。
以 re
標籤實現一個動態的正則表達式構造器,這種運行時生成的正則表達式,一般要本身定義函數,並調用 new RegExp 實現。
var match = input.match(re`\d+${separator}\d+`)
複製代碼
能夠實現這樣一個 i18n
和 l10n
模板標籤:約定在模板字符串中的插值表達式 ${expression}
後,以 :type
格式指定 expression 表達式指望的文本顯示格式,實現自定義的模板輸出功能。
var message = l10n`Hello ${name}; you are visitor number ${visitor}:n! You have ${money}:c in your account!`;
複製代碼
這裏的 l10n
標籤與咱們在上文 hard-code 的版本相比,增長了 :type
標識符以表示類別,例如 :n
表示數字,:c
表示貨幣。這些類型標識符的規則能夠在 l10n
的實現代碼中約定。而這個約定的意味就有點自行定義 DSL 的味道了。
以上 4 個 case 的共同點是,咱們首先約定了有類似模式的 API 接口,它們都表現爲帶標籤的模板的形式——一個模板名後跟模板內容。
雖然咱們做爲實現者知道,實際上在調用標籤模板時,本質上是將模板內容重組爲 (strings, ...rest)
形式再傳給標籤函數調用的。但這樣的 API 調用看上去卻很像是隻有一個函數和一個參數,讓人一眼看到就能猜出來 API 的用途。
好的 API 應當有良好的自我描述性,將複雜的實現細節封裝起來,而且儘可能專一作好一件事。從這個角度來講帶標籤的 ES6 模板很是適合處理 JS 內嵌的 DSL,甚至能夠幫助咱們在特定的業務邏輯中實現一個 mini DSL。
以上就是對 ES6 模板語法和實用價值的介紹。講到實踐,得益於其原理的簡潔,咱們能夠當即享受到它帶來的好處。在 Node.js 環境下,毫無疑問咱們能夠當即使用不用遲疑;在瀏覽器環境下,使用咱們的老朋友 Babel 就能夠將其轉換爲兼容的 ES5 代碼。
總結起來,ES6 模板中最激動人心的特性仍是標籤,小小的標籤用簡單的原理提供了異常豐富的擴展能力,很有點四兩撥千金的感受。基於它,JavaScript 社區已經產生了不少新的想法併產生了不少實實在在的工具庫。
除了咱們在前面示例中有簡單提到 common-tags
和 nano-html
兩個庫,也有不少實現了特定領域功能的標籤庫,例如 SQL 相關的、國際化和本地化相關的,用 tagged template literals 這幾個關鍵詞在 npm 搜索就能夠找到別人造的輪子。
但相比其餘主題,社區關注量最大的探索仍是集中在將模板字面量與 HTML 模板的結合上,有 3 個表明性的框架致力於採用 template literals 的方案並結合其餘 good stuff 實現能夠媲美 Virtual DOM 的快速渲染方案。
<template>
元素的優勢,實現 DOM 元素的快速渲染和更新這 3 個框架的共同點是都採用了 tagged template literals,而且放棄使用 Virtual DOM 這種如今很是火爆的概念,但號稱同樣能實現快速渲染和更新真實 DOM。從各自提供的數據上看,也的確都有着不俗的表現。礙於篇幅在這裏咱們就再也不展開討論了。
這些框架也反映出 ES6 模板的確潛力很大,更深刻的就留給咱們將來探索,如下這些文章都是不錯的參考。
本文同步發表在 SegmentFault 和 我的博客