對 ECMAScript 6 准文字的批判性評論
Quasi-literals(更新:現在正式稱為“模板字符串”)是 ECMAScript 6 的一項提議添加,旨在解決一系列問題。該提案旨在添加允許創建特定領域語言 (DSL) 1 的新語法 以比我們今天的解決方案更安全的方式處理內容。 模板字符串文字稻草人頁面上的描述 2 如下:
但實際上,模板字符串是 ECMAScript 對幾個持續存在的問題的解決方案。據我所知,這些是模板字符串試圖解決的直接問題:
- 多行字符串 – JavaScript 從來沒有一個正式的多行字符串概念。
- 基本字符串格式 – 能夠用部分字符串替換變量中包含的值。
- HTML 轉義 – 轉換字符串以便安全插入 HTML 的能力。
- 字符串的本地化 – 能夠輕鬆地將字符串從一種語言換成另一種語言的字符串。
我一直在查看模板字符串,以確定它們是否真的充分解決了這些問題。我最初的反應是,模板字符串在某些情況下解決了其中一些問題,但不足以成為解決這些問題的唯一機制。我決定花一些時間探索模板字符串,以確定我的反應是否有效。
基礎知識
在深入研究用例之前,了解模板字符串的工作原理很重要。模板字符串的基本格式如下:
`literal${substitution}literal`
這是最簡單的模板字符串形式,只是簡單地進行替換。整個模板字符串用反引號括起來。在這些反引號之間可以是任意數量的字符,包括空格。美元符號($
) 表示應該被替換的表達式。在此示例中,模板字符串將替換 ${substitution}
使用名為 substitution
的 JavaScript 變量的值 這在定義模板字符串的同一範圍內可用。例如:
var name = "Nicholas",
msg = `Hello, ${name}!`;
console.log(msg); // "Hello, Nicholas!"
在此代碼中,模板字符串有一個要替換的標識符。序列${name}
替換為變量 name
的值 .您可以替換更複雜的表達式,例如:
var total = 30,
msg = `The total is ${total} (${total*1.05} with tax)`;
console.log(msg); // "The total is 30 (31.5 with tax)"
此示例使用更複雜的表達式替換來計算含稅價格。您可以將任何返回值的表達式放在模板字符串的大括號內,以將該值插入到最終字符串中。
更高級的模板字符串格式如下:
tag`literal${substitution}literal`
這種形式包括一個標籤,它基本上只是一個改變模板字符串輸出的函數。模板字符串提案包括一個針對幾個內置標籤的提案來處理常見情況(這些將在後面討論),但也可以定義自己的。
標記只是一個使用已處理的模板字符串數據調用的函數。該函數接收關於模板字符串的數據作為單獨的片段,然後標籤必須組合這些數據以創建最終值。函數接收的第一個參數是一個數組,其中包含由 JavaScript 解釋的文字字符串。這些數組的組織方式是在項目之間進行替換,因此需要在第一個和第二個項目、第二個和第三個項目之間進行替換,依此類推。這個數組還有一個特殊的屬性叫做 raw
,它是一個數組,其中包含出現在代碼中的文字字符串(因此您可以知道代碼中寫了什麼)。第一個之後的標記的每個後續參數都是模板字符串中替換錶達式的值。例如,這是將傳遞給最後一個示例的標籤的內容:
- 參數 1 =
[ "The total is ", " (", " with tax)" ]
.raw = [ "The total is ", " (", " with tax)" ]
- 參數 2 =
30
- 參數 3 =
31.5
請注意,替換錶達式是自動計算的,因此您只收到最終值。這意味著標籤可以以任何適當的方式自由地操縱最終值。例如,我可以創建一個行為與默認值相同的標籤(未指定標籤時),如下所示:
function passthru(literals) {
var result = "",
i = 0;
while (i < literals.length) {
result += literals[i++];
if (i < arguments.length) {
result += arguments[i];
}
}
return result;
}</code>
然後你可以這樣使用它:
var total = 30,
msg = passthru`The total is ${total} (${total*1.05} with tax)`;
console.log(msg); // "The total is 30 (31.5 with tax)"
在所有這些示例中,raw
之間沒有區別 和 cooked
因為模板字符串中沒有特殊字符。考慮這樣的模板字符串:
tag`First line\nSecond line`
在這種情況下,標籤會收到:
- 參數 1 =
cooked = [ "First line\nSecond line" ]
.raw = [ "First line\\nSecond line" ]
注意 raw
中的第一項 是字符串的轉義版本,實際上與用代碼編寫的內容相同。可能並不總是需要這些信息,但以防萬一。
多行字符串
第一個問題是模板字符串文字旨在解決他的多行字符串。正如我在之前的帖子中提到的,這對我來說不是什麼大問題,但我知道有相當多的人想要這種能力。多年來,一直有一種非官方的方式來處理多行字符串文字和 JavaScript,使用反斜杠後跟換行符,例如:
var text = "First line\n\
Second line";
這被廣泛認為是一個錯誤並且被認為是一種不好的做法,儘管它作為 ECMAScript 5 的一部分得到了祝福。許多人為了不使用非官方技術而使用數組:
var text = [
"First line",
"Second line"].join("\n");
但是,如果您要編寫大量文本,這將非常麻煩。有一種方法將
直接包含在文字中肯定會更容易。其他語言多年來一直具有此功能。
這里當然有docs 3 ,比如PHP支持什麼:
$text = <<<EOF
First line
Second line
EOF;
Python 有它的三重引號字符串:
text = """First line
Second line"""
相比之下,模板字符串文字看起來更乾淨,因為它們使用的字符更少:
var text = `First line
Second line`;
所以很容易看出模板字符串很好地解決了 JavaScript 中的多行字符串問題。這無疑是需要新語法的情況,因為雙引號和單引號都已經被使用(並且幾乎完全相同)。
基本字符串格式化
基本字符串格式的問題在 JavaScript 中還沒有解決。當我說基本的字符串格式時,我指的是文本中的簡單替換。想想 sprintf
在 C 或 String.format()
中 在 C# 或 Java 中。這條評論並不是專門針對 JavaScript 的,它在開發的幾個角落找到了生命。
一、console.log()
方法(及其相關方法)支持 Internet Explorer 8+、Firefox、Safari 和 Chrome 中的基本字符串格式化(Opera 不支持控制台上的字符串格式化)。在服務器上,Node.js 還支持其 console.log()
的字符串格式化
4
.您可以包含 %s
替換字符串,%d
或 %i
替換一個整數,或 %f
對於浮點值(Node.js 也允許 %j
包括 JSON、Firefox 和 Chrome 允許 %o
用於輸出對象
5
)。例如:
console.log("Hello %s", "world"); // "Hello world"
console.log("The count is %d", 5); // "The count is 5"
各種 JavaScript 庫也實現了類似的字符串格式化功能。 YUI 有 substitute()
6
方法,它使用命名值進行字符串替換:
YUI().use("substitute", function(Y) {
var msg = Y.substitute("Hello, {place}", { place: "world" });
console.log(msg); // "Hello, world"
});
Dojo 通過 dojo.string.substitute()
有類似的機制
7
, 雖然它也可以通過傳遞一個數組來處理位置替換:
var msg = dojo.string.substitue("Hello, ${place}", { place: "world" });
console.log(msg); // "Hello, world"
msg = dojo.string.substitue("Hello, ${0}", [ "world" ]);
console.log(msg); // "Hello, world"
很明顯,基本的字符串格式化在 JavaScript 中已經存在並且很好,而且很可能許多開發人員在某個時間點都使用過它。請記住,簡單的字符串格式化與值的轉義無關,因為它進行的是簡單的字符串操作(HTML 轉義將在後面討論)。
與已經可用的字符串格式化方法相比,模板字符串在視覺上看起來非常相似。以下是使用模板字符串編寫前面示例的方式:
var place = "world",
msg = `Hello, ${place}`;
console.log(msg); // "Hello, world"
從語法上講,有人可能會爭辯說模板字符串更容易閱讀,因為變量直接放在文字中,因此您可以更輕鬆地猜測結果。因此,如果您打算將使用較舊的字符串格式化方法的代碼轉換為模板字符串,那麼如果您直接在 JavaScript 中使用字符串字面量,這將是一個非常簡單的轉換。
模板字符串的缺點與使用 heredocs 的缺點相同:文字必須在可以訪問替換變量的範圍內定義。這有幾個問題。首先,如果在定義模板字符串的範圍內沒有定義替換變量,則會拋出錯誤。例如:
var msg = `Hello, ${place}`; // throws error
因為 place
在這個例子中沒有定義,模板字符串實際上會拋出一個錯誤,因為它試圖評估不存在的變量。這種行為也是模板字符串的第二個主要問題的原因:您不能將字符串外部化。
使用簡單字符串格式時,如 console.log()
、YUI 或 Dojo,您可以將字符串保存在使用它的 JavaScript 代碼之外。這樣做的好處是使字符串更改更容易(因為它們沒有隱藏在 JavaScript 代碼中)並允許在多個地方使用相同的字符串。例如,您可以在一個地方定義您的字符串,如下所示:
var messages = {
welcome: "Hello, {name}"
};
並像這樣在其他地方使用它們:
var msg = Y.substitute(messages.welcome, { name: "Nicholas" });
使用模板字符串,您只能在文字直接嵌入 JavaScript 以及表示要替換的數據的變量時使用替換。實際上,格式字符串與數據值具有後期綁定,而模板字符串與數據值具有早期綁定。這種早期綁定嚴重限制了模板字符串可用於簡單替換的情況。
因此,當您想在 JavaScript 代碼中嵌入文字時,雖然模板字符串解決了簡單的字符串格式化問題,但當您想將字符串外部化時,它們並不能解決問題。出於這個原因,我相信即使添加了模板字符串,也需要在 ECMAScript 中添加一些基本的字符串格式化功能。
字符串本地化
與簡單字符串格式化密切相關的是字符串的本地化。本地化是一個複雜的問題,涵蓋了 Web 應用程序的所有方面,但字符串的本地化是模板字符串應該幫助解決的問題。基本思想是您應該能夠用一種語言定義帶有佔位符的字符串,並且能夠輕鬆地將字符串翻譯成使用相同替換的另一種語言。
這在當今大多數係統中的工作方式是將字符串外部化為單獨的文件或數據結構。兩個 YUI 9 和道場 10 支持國際化的外部化資源包。從根本上說,它們的工作方式與簡單的字符串格式化相同,其中每個字符串都是對像中的一個單獨屬性,可以在任意數量的地方使用。字符串還可以包含用於替換庫方法的佔位符。例如:
// YUI
var messages = Y.Intl.get("messages");
console.log(messages.welcome, { name: "Nicholas" });
由於字符串中的佔位符永遠不會改變,無論語言如何,JavaScript 代碼都保持相當簡潔,不需要考慮不同語言中的不同單詞順序和替換之類的事情。
模板字符串似乎推薦的方法更像是一種基於工具的過程。稻草人提案談到了一個特殊的msg
能夠處理本地化字符串的標籤。 msg
的用途 只是為了確保替換本身的格式正確,適合當前的語言環境(由開發人員定義)。除此之外,它似乎只做基本的字符串替換。其目的似乎是允許對 JavaScript 文件進行靜態分析,以便可以生成一個新的 JavaScript 文件,該文件正確地將模板字符串的內容替換為適合該語言環境的文本。給出的第一個示例是假設您已經在某處擁有翻譯數據,將英語翻譯成法語:
// Before
alert(msg`Hello, ${world}!`);
// After
alert(msg`Bonjour ${world}!`);
目的是通過一些尚未定義的工具將第一行轉換為第二行。對於那些不想使用此工具的人,該提案建議將消息包包含在行中,以便 msg
標籤在該包中查找其數據以進行適當的替換。這是那個例子:
// Before
alert(msg`Hello, ${world}!`);
// After
var messageBundle_fr = { // Maps message text and disambiguation meta-data to replacement.
'Hello, {0}!': 'Bonjour {0}!'
};
alert(msg`Hello, ${world}!`);
目的是在投入生產之前將第一行翻譯成之後的幾行。您會注意到,為了完成這項工作,消息包使用了格式字符串。 msg
標記然後寫為:
function msg(parts) {
var key = ...; // 'Hello, {0}!' given ['Hello, ', world, '!']
var translation = myMessageBundle[key];
return (translation || key).replace(/\{(\d+)\}/g, function (_, index) {
// not shown: proper formatting of substitutions
return parts[(index < < 1) | 1];
});
}</code>
因此,似乎為了避免格式化字符串,模板字符串僅通過實現自己的簡單字符串格式化來實現本地化目的。
對於這個問題,似乎我實際上是在將蘋果與橙子進行比較。 YUI 和 Dojo 處理本地化字符串和資源包的方式非常適合開發人員。模板字符串方法非常適合工具,因此對於不想經歷將額外工具集成到構建系統中的麻煩的人來說不是很有用。我不相信提案中的本地化方案相對於開發人員已經在做的事情有很大的優勢。
HTML 轉義
這可能是模板字符串要解決的最大問題。每當我在 TC-39 上與人們談論模板字符串時,談話似乎總是回到安全轉義以插入 HTML。該提案本身首先討論了跨站點腳本攻擊以及模板字符串如何幫助緩解這些攻擊。毫無疑問,正確的 HTML 轉義對於任何 Web 應用程序都很重要,無論是在客戶端還是在服務器上。幸運的是,我們已經看到一些更符合邏輯的排版語言彈出,例如 Mustache,默認自動轉義輸出。
在談論 HTML 轉義時,重要的是要了解有兩種不同的數據類別。第一類數據是受控的。受控數據是由服務器在沒有任何用戶交互的情況下生成的數據。也就是說,數據是開發者編寫的,不是用戶輸入的。另一類數據是不受控制的,這正是模板字符串打算處理的事情。不受控制的數據是來自用戶的數據,因此您不能對其內容做出任何假設。反對格式字符串的一大論據是不受控制的格式字符串的威脅 11 以及它們可能造成的損害。當不受控制的數據被傳遞到格式字符串中並且在此過程中沒有正確轉義時,就會發生這種情況。例如:
// YUI
var html = Y.substitute(">p<Welcome, {name}>/p<", { name: username });
在此代碼中,如果 username
生成的 HTML 可能存在安全問題 在此之前尚未進行消毒。 username
有可能 可能包含 HTML 代碼,尤其是 JavaScript,這可能會破壞插入字符串的頁面。這在瀏覽器上可能不是什麼大問題,通過 innerHTML
插入腳本標籤是無害的 ,但是在服務器上這肯定是一個主要問題。 YUI 有 Y.Escape.html()
轉義可用於幫助的 HTML:
// YUI
YUI().use("substitute", "escape", function(Y) {
var escapedUsername = Y.Escape.html(username),
html = Y.substitute(">p<Welcome, {name}>/p<", { name: escapedUsername });
});
在 HTML 轉義之後,用戶名在被插入到字符串之前會被稍微清理一下。這為您提供了針對不受控制的數據的基本保護。問題可能會比這更複雜一些,尤其是當您處理插入到 HTML 屬性中的值時,但本質上在插入 HTML 字符串之前轉義 HTML 是清理數據應該做的最低限度。
模板字符串旨在解決 HTML 轉義問題以及其他一些問題。該提案討論了一個名為 safehtml
的標籤 ,它不僅會執行 HTML 轉義,還會尋找其他攻擊模式並用無害的值替換它們。提案中的例子是:
url = "http://example.com/";
message = query = "Hello & Goodbye";
color = "red";
safehtml`<a href="${url}?q=${query}" onclick=alert(${message}) style="color: ${color}">${message}</a>`
在這種情況下,HTML 文字中有幾個潛在的安全問題。 URL 本身最終可能是一個執行錯誤的 JavaScript URL,查詢字符串也可能最終是錯誤的,並且 CSS 值最終可能是舊版本 Internet Explorer 中的 CSS 表達式。例如:
url = "javascript:alert(1337)";
color = "expression(alert(1337))";
使用簡單的 HTML 轉義將這些值插入到字符串中,如前面的示例所示,不會阻止生成的 HTML 包含危險代碼。 HTML 轉義的 JavaScript URL 仍然執行 JavaScript。 safehtml
的意圖 就是不僅要處理 HTML 轉義,還要處理這些攻擊場景,其中一個值是危險的,不管它是否被轉義。
模板字符串提案聲稱,在 JavaScript URL 等情況下,這些值將被替換為完全無害的東西,因此可以防止任何傷害。它沒有涵蓋的是標籤如何知道“危險”值是否實際上是受控數據和有意插入的數據,而不是應始終更改的不受控數據。閱讀該提案後我的預感是,它總是假定危險值是危險的,並且由開發人員決定是否跳過一些可能對標籤看起來很危險的代碼。這不一定是壞事。
那麼模板字符串是否解決了 HTML 轉義問題?與簡單的字符串格式化一樣,答案是肯定的,但前提是您將 HTML 嵌入到存在替換變量的 JavaScript 中。將 HTML 直接嵌入 JavaScript 是我警告人們不要做的事情,因為它變得難以維護。使用諸如 Mustache 之類的模板解決方案,模板通常在運行時從某個地方讀入,或者預編譯成直接執行的函數。似乎 safehtml
的目標受眾 標籤實際上可能是模板庫。在編譯模板時,我肯定會看到這很有用。可以使用 safehtml
將模板編譯成模板字符串,而不是編譯成複雜的函數 標籤。這將消除模板語言的一些複雜性,儘管我敢肯定不是全部。
由於沒有使用工具從字符串或模板生成模板字符串,我很難相信開發人員會在創建一個簡單的 HTML 轉義函數如此簡單時經歷使用它們的麻煩。這是我傾向於使用的一個:
function escapeHTML(text) {
return text.replace(/[<>"&]/g, function(c) {
switch (c) {
case "< ": return "<";
case ">": return ">";
case "\"": return """;
case "&": return "&";
}
});
}
我認識到進行基本的 HTML 轉義不足以完全保護 HTML 字符串免受所有威脅。但是,如果基於模板字符串的 HTML 處理必須直接在 JavaScript 代碼中完成,我認為許多開發人員最終仍會使用基本的 HTML 轉義。庫已經為開發人員提供了這個功能,如果我們能有一個每個人都可以依賴的標準版本,那就太好了,這樣我們就可以停止向每個庫發布相同的東西。與 msg
一樣 標籤,需要簡單的字符串格式才能正常工作,我還可以看到 safehtml
需要基本的 HTML 轉義才能正常工作。他們似乎齊頭並進。
結論
模板字符串絕對解決了我在本文開頭概述的所有四個問題。它們最成功地解決了 JavaScript 中對多行字符串文字的需求。該解決方案可以說是最優雅的解決方案,並且做得很好。
當涉及到簡單的字符串格式化時,模板字符串解決問題的方式與 heredocs 解決問題的方式相同。如果您要將字符串直接嵌入到替換變量所在的代碼中,那就太好了。如果您需要將字符串外部化,那麼模板字符串並不能為您解決問題。鑑於許多開發人員將字符串外部化為包含在其應用程序中的資源包,我對模板字符串解決許多開發人員的字符串格式化需求的潛力持悲觀態度。我相信基於格式字符串的解決方案,例如 Crockford 提出的解決方案 [12] , 仍然需要成為 ECMAScript 的一部分才能完整併徹底解決這個問題。
我完全不相信模板字符串可以解決本地化用例。似乎這個用例是硬塞進去的,當前的解決方案需要的工作量要少得多。當然,我發現本地化模板字符串解決方案最有趣的部分是它使用了格式字符串。對我來說,這是一個明顯的信號,即在 ECMAScript 中絕對需要簡單的字符串格式化。模板字符串似乎是解決本地化問題的最嚴厲的解決方案,即使使用提案中提到的尚未創建的工具也是如此。
模板字符串確實解決了 HTML 轉義問題,但同樣,僅以解決簡單字符串格式化的方式。由於需要將 HTML 嵌入到 JavaScript 中並在該範圍內存在所有變量,safehtml
tag 似乎只從模板工具的角度有用。開發人員似乎不會手動使用它,因為許多人都在使用外部化模板。如果預編譯模板的模板庫是此功能的目標受眾,那麼它就有機會獲得成功。但是,我認為它不能滿足其他開發人員的需求。我仍然相信 HTML 轉義雖然容易出錯,但需要作為 ECMAScript 中的低級方法提供。
注意:我知道有很多人認為 HTML 轉義不一定是 ECMAScript 的一部分。有人說它應該是瀏覽器 API、DOM 的一部分或其他東西。我不同意這種觀點,因為在客戶端和服務器上都非常頻繁地使用 JavaScript 來操作 HTML。因此,我認為 ECMAScript 支持 HTML 轉義和 URL 轉義很重要(它已經支持了很長時間)。
總的來說,模板字符串是一個有趣的概念,我認為它很有潛力。他們立即解決了 JavaScript 中多行字符串和類似 heredocs 的功能的問題。作為工具的生成目標,它們似乎也是一個有趣的解決方案。我不認為它們是 JavaScript 開發人員簡單字符串格式或低級 HTML 轉義的合適替代品,這兩者在標籤中都可能有用。我並不是要求從 ECMAScript 中刪除模板字符串,但我確實認為它並沒有為 JavaScript 開發人員解決足夠多的問題,它應該排除對字符串格式和轉義的其他添加。
更新(2012 年 8 月 1 日) – 更新文章提到模板字符串中始終需要大括號。此外,通過將“准文字”更改為“模板字符串”和“準處理程序”更改為“標籤”,解決了艾倫評論中的一些反饋。更新了斜杠結尾的多行字符串的描述。
更新(2012 年 8 月 2 日) - 修正了基於 Ryan 評論的 YUI 方法名稱。修復 escapeHTML()
根據 Jakub 的評論,函數編碼問題。
參考
- 領域特定語言(維基百科)
- ECMAScript 准文字(ECMAScript Wiki)
- 此處-文檔(維基百科)
- Charlie McConnell (Nodejitsu) 的內置控制台模塊
- 向控制台輸出文本(Mozilla 開發者網絡)
- YUI 替代方法 (YUILibrary)
- dojo.string.substitute()(Dojo 工具包)
- YUI 國際化 (YUILibrary)
- Adam Peller 的可翻譯資源包
- 不受控制的格式字符串(維基百科)
- Douglas Crockford 的 String.prototype.format(ECMAScript Wiki)