\n)String.rawES6 有两种新的字面量:模板字面量 和 带标签的模板字面量。这两种字面量名称相似,外观也相似,但它们却截然不同。因此,区分以下几点非常重要
模板字面量 是可以跨越多行并包含插值表达式(通过 ${···} 插入)的字符串字面量
const firstName = 'Jane';
console.log(`Hello ${firstName}!
How are you
today?`);
// Output:
// Hello Jane!
// How are you
// today?
带标签的模板字面量(简称:带标签的模板)是通过在模板字面量之前提及函数来创建的
> String.raw`A \tagged\ template`
'A \\tagged\\ template'
带标签的模板是函数调用。在上一个示例中,调用方法 String.raw 来生成带标签的模板的结果。
字面量是生成值的语法结构。例如,字符串字面量(生成字符串)和正则表达式字面量(生成正则表达式对象)。ECMAScript 6 有两种新的字面量
重要的是要记住,模板字面量和带标签的模板的名称略有误导性。它们与 Web 开发中经常使用的模板无关:模板是包含可以通过(例如)JSON 数据填充的空白的文本文件。
模板字面量是一种新型的字符串字面量,它可以跨越多行并插入表达式(包含其结果)。例如
const firstName = 'Jane';
console.log(`Hello ${firstName}!
How are you
today?`);
// Output:
// Hello Jane!
// How are you
// today?
字面量本身由反引号 (`) 分隔,字面量内的插值表达式由 ${ 和 } 分隔。模板字面量始终生成字符串。
反斜杠用于在模板字面量中进行转义。
它使您能够在模板字面量中提及反引号和 ${
> `\``
'`'
> `$` // OK
'$'
> `${`
SyntaxError
> `\${`
'${'
> `\${}`
'${}'
除此之外,反斜杠的工作方式与字符串字面量相同
> `\\`
'\\'
> `\n`
'\n'
> `\u{58}`
'X'
\n) 终止行的常见方法是
\n, U+000A):由 Unix(包括当前的 macOS)使用\r, U+000D):由旧的 Mac OS 使用。\r\n):由 Windows 使用。所有这些行终止符在模板字面量中都规范化为 LF。也就是说,以下代码在所有平台上都记录 true
const str = `BEFORE
AFTER`;
console.log(str === 'BEFORE\nAFTER'); // true
以下是带标签的模板字面量(简称:带标签的模板)
tagFunction`Hello ${firstName} ${lastName}!`
在表达式后放置模板字面量会触发函数调用,类似于参数列表(括号中以逗号分隔的值)触发函数调用的方式。前面的代码等效于以下函数调用(实际上,第一个参数不仅仅是一个数组,但稍后会解释)。
tagFunction(['Hello ', ' ', '!'], firstName, lastName)
因此,反引号中内容之前的名称是要调用的函数的名称,即标签函数。标签函数接收两种不同类型的数据
'Hello '。firstName(由 ${} 分隔)。替换可以是任何表达式。模板字符串是静态已知的(在编译时),而替换仅在运行时才知道。标签函数可以随意处理其参数:它可以完全忽略模板字符串,返回任何类型的值,等等。
此外,标签函数会获取每个模板字符串的两个版本
`\n` 变为 '\\n',一个长度为 2 的字符串)`\n` 变为仅包含换行符的字符串)。这允许 String.raw(稍后解释)完成其工作
> String.raw`\n` === '\\n'
true
带标签的模板字面量允许您轻松实现自定义嵌入式子语言(有时称为领域特定语言),因为 JavaScript 会为您完成大部分解析工作。您只需编写一个接收结果的函数。
让我们看一些例子。其中一些例子受到模板字面量的原始提案的启发,该提案通过其旧名称准字面量来指代它们。
ES6 包含用于原始字符串的标签函数 String.raw,其中反斜杠没有特殊含义
const str = String.raw`This is a text
with multiple lines.
Escapes are not interpreted,
\n is not a newline.`;
每当您需要创建包含反斜杠的字符串时,这都很有用。例如
function createNumberRegExp(english) {
const PERIOD = english ? String.raw`\.` : ','; // (A)
return new RegExp(`[0-9]+(${PERIOD}[0-9]+)?`);
}
在 A 行中,String.raw 使我们能够像在正则表达式字面量中那样编写反斜杠。使用普通的字符串字面量,我们必须转义两次:首先,我们需要为正则表达式转义点。其次,我们需要为字符串字面量转义反斜杠。
const proc = sh`ps ax | grep ${pid}`;
(来源:David Herman)
const buffer = bytes`455336465457210a`;
(来源:David Herman)
POST`http://foo.org/bar?a=${a}&b=${b}
Content-Type: application/json
X-Credentials: ${credentials}
{ "foo": ${foo},
"bar": ${bar}}
`
(myOnReadyStateChangeHandler);
(来源:Luke Hoban)
Steven Levithan 举了一个例子,说明如何将带标签的模板字面量用于他的正则表达式库 XRegExp。
如果没有带标签的模板,则需要编写如下代码
var parts = '/2015/10/Page.html'.match(XRegExp(
'^ # match at start of string only \n' +
'/ (?<year> [^/]+ ) # capture top dir name as year \n' +
'/ (?<month> [^/]+ ) # capture subdir name as month \n' +
'/ (?<title> [^/]+ ) # capture base name as title \n' +
'\\.html? $ # .htm or .html file ext at end of path ', 'x'
));
console.log(parts.year); // 2015
我们可以看到 XRegExp 为我们提供了命名组(year、month、title)和 x 标志。使用该标志,大多数空格将被忽略,并且可以插入注释。
字符串字面量在这里不能很好地工作有两个原因。首先,我们必须将每个正则表达式反斜杠键入两次,以便为字符串字面量对其进行转义。其次,输入多行很麻烦。
除了添加字符串之外,如果使用反斜杠结束当前行,您还可以在下一行继续字符串字面量。但这仍然涉及很多视觉混乱,特别是因为您仍然需要在每行的末尾通过 \n 显式换行。
var parts = '/2015/10/Page.html'.match(XRegExp(
'^ # match at start of string only \n\
/ (?<year> [^/]+ ) # capture top dir name as year \n\
/ (?<month> [^/]+ ) # capture subdir name as month \n\
/ (?<title> [^/]+ ) # capture base name as title \n\
\\.html? $ # .htm or .html file ext at end of path ', 'x'
));
使用带标签的模板可以解决反斜杠和多行问题
var parts = '/2015/10/Page.html'.match(XRegExp.rx`
^ # match at start of string only
/ (?<year> [^/]+ ) # capture top dir name as year
/ (?<month> [^/]+ ) # capture subdir name as month
/ (?<title> [^/]+ ) # capture base name as title
\.html? $ # .htm or .html file ext at end of path
`);
此外,带标签的模板允许您通过 ${v} 插入值 v。我希望正则表达式库能够转义字符串并逐字插入正则表达式。例如
var str = 'really?';
var regex = XRegExp.rx`(${str})*`;
这等效于
var regex = XRegExp.rx`(really\?)*`;
示例
$`a.${className}[href*='//${domain}/']`
这是一个 DOM 查询,它查找所有 CSS 类为 className 且目标是具有给定域的 URL 的 <a> 标签。标签函数 $ 确保参数被正确转义,从而使这种方法比手动字符串连接更安全。
Facebook React 是“用于构建用户界面的 JavaScript 库”。它具有可选的语言扩展 JSX,使您能够为用户界面构建虚拟 DOM 树。此扩展使您的代码更简洁,但它也是非标准的,并且会破坏与 JavaScript 生态系统其余部分的兼容性。
库 t7.js 提供了 JSX 的替代方案,并使用带有 t7 标签的模板
t7.module(function(t7) {
function MyWidget(props) {
return t7`
<div>
<span>I'm a widget ${ props.welcome }</span>
</div>
`;
}
t7.assign('Widget', MyWidget);
t7`
<div>
<header>
<Widget welcome="Hello world" />
</header>
</div>
`;
});
在“为什么不使用模板字面量?”中,React 团队解释了他们为什么选择不使用模板字面量。一个挑战是在带标签的模板中访问组件。例如,在前面的示例中,从第二个带标签的模板访问 MyWidget。一种冗长的做法是
<${MyWidget} welcome="Hello world" />
相反,t7.js 使用通过 t7.assign() 填充的注册表。这需要额外的配置,但模板字面量看起来更好;尤其是在同时存在开始标签和结束标签的情况下。
Facebook Relay 是“用于构建数据驱动的 React 应用程序的 JavaScript 框架”。它的其中一部分是查询语言 GraphQL,其查询可以通过带有 Relay.QL 标签的模板创建。例如(摘自 Relay 主页)
class Tea extends React.Component {
render() {
var {name, steepingTime} = this.props.tea;
return (
<li key={name}>
{name} (<em>{steepingTime} min</em>)
</li>
);
}
}
Tea = Relay.createContainer(Tea, {
fragments: { // (A)
tea: () => Relay.QL`
fragment on Tea {
name,
steepingTime,
}
`,
},
});
class TeaStore extends React.Component {
render() {
return <ul>
{this.props.store.teas.map(
tea => <Tea tea={tea} />
)}
</ul>;
}
}
TeaStore = Relay.createContainer(TeaStore, {
fragments: { // (B)
store: () => Relay.QL`
fragment on Store {
teas { ${Tea.getFragment('tea')} },
}
`,
},
});
从 A 行和 B 行开始的对象定义了片段,这些片段是通过返回查询的回调定义的。片段 tea 的结果放入 this.props.tea 中。片段 store 的结果放入 this.props.store 中。
这是查询操作的数据
const STORE = {
teas: [
{name: 'Earl Grey Blue Star', steepingTime: 5},
···
],
};
此数据包装在一个 GraphQLSchema 实例中,并在其中获得名称 Store(如 fragment on Store 中所述)。
本节介绍一种简单的文本本地化方法,它支持不同的语言和不同的地区(如何格式化数字、时间等)。假设有以下消息。
alert(msg`Welcome to ${siteName}, you are visitor
number ${visitorNumber}:d!`);
标签函数 msg 的工作原理如下。
首先,将字面量部分连接起来形成一个字符串,该字符串可用于在表中查找翻译。上一个示例的查找字符串是
'Welcome to {0}, you are visitor number {1}!'
例如,此查找字符串可以映射到德语翻译:
'Besucher Nr. {1}, willkommen bei {0}!'
英语“翻译”将与查找字符串相同。
其次,使用查找结果来显示替换。由于查找结果包含索引,因此它可以重新排列替换的顺序。这已经在德语中完成,其中访问者编号位于站点名称之前。如何格式化替换可以通过注释来影响,例如 :d。此注释表示应为 visitorNumber 使用特定于地区的十进制分隔符。因此,可能的英语结果是
Welcome to ACME Corp., you are visitor number 1,300!
在德语中,我们有如下结果
Besucher Nr. 1.300, willkommen bei ACME Corp.!
假设我们要创建 HTML,将以下数据显示在表格中
const data = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
如前所述,模板字面量不是模板
模板基本上是一个函数:输入数据,输出文本。该描述为我们提供了一个线索,说明如何将模板字面量转换为实际模板。让我们实现一个模板 tmpl 作为将数组 addrs 映射到字符串的函数
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join('')}
</table>
`;
console.log(tmpl(data));
// Output:
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
外部模板字面量提供括号 <table> 和 </table>。在内部,我们嵌入了 JavaScript 代码,该代码通过连接字符串数组来生成字符串。该数组是通过将每个地址映射到两行表格来创建的。请注意,纯文本片段 <Jane> 和 <Croft> 未正确转义。下一节将解释如何通过标记模板执行此操作。
对于较小的模板任务,这是一个有用的快速解决方案。对于较大的任务,您可能需要更强大的解决方案,例如模板引擎 Handlebars.js 或 React 中使用的 JSX 语法。
**致谢:**这种文本模板化方法基于 Claus Reinke 的一个想法。
与上一节中使用无标签模板进行 HTML 模板化相比,标记模板有两个优点
${} 前面加上感叹号,它们可以为我们转义字符。这是名称所必需的,其中包含需要转义的字符 (<Jane>)。join() 数组,因此我们不必自己调用该方法。然后模板的代码如下所示。标签函数的名称是 html
const tmpl = addrs => html`
<table>
${addrs.map(addr => html`
<tr><td>!${addr.first}</td></tr>
<tr><td>!${addr.last}</td></tr>
`)}
</table>
`;
const data = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
console.log(tmpl(data));
// Output:
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
请注意,Jane 和 Croft 周围的尖括号已转义,而 tr 和 td 周围的尖括号未转义。
如果在替换前面加上感叹号 (!${addr.first}),则它将进行 HTML 转义。标签函数检查替换前的文本以确定是否进行转义。
html 的实现稍后显示。
以下是一个标记模板字面量
tagFunction`lit1\n${subst1} lit2 ${subst2}`
此字面量(大致)触发以下函数调用
tagFunction(['lit1\n', ' lit2 ', ''], subst1, subst2)
确切的函数调用看起来更像这样
// Globally: add template object to per-realm template map
{
// “Cooked” template strings: backslash is interpreted
const templateObject = ['lit1\n', ' lit2 ', ''];
// “Raw” template strings: backslash is verbatim
templateObject.raw = ['lit1\\n', ' lit2 ', ''];
// The Arrays with template strings are frozen
Object.freeze(templateObject.raw);
Object.freeze(templateObject);
__templateMap__[716] = templateObject;
}
// In-place: invocation of tag function
tagFunction(__templateMap__[716], subst1, subst2)
标签函数接收两种输入
' lit2 ')。模板对象存储模板字符串的两个版本\n 等转义符。存储在 templateObject[0] 等中。templateObject.raw[0] 等中。${} 嵌入模板字面量中的值(例如 subst1)。替换是动态的,它们可以随着每次调用而改变。全局模板对象背后的想法是,相同的标记模板可能会执行多次(例如,在循环或函数中)。模板对象使标签函数能够缓存来自先前调用的数据:它可以将其从输入类型 #1(模板字符串)派生的数据放入对象中,以避免重新计算它。缓存按_领域_发生(想想浏览器中的框架)。也就是说,每个调用站点和领域都有一个模板对象。
让我们使用以下标签函数来探索有多少个模板字符串与替换相比。
function tagFunc(templateObject, ...substs) {
return { templateObject, substs };
}
模板字符串的数量始终比替换的数量多一个。换句话说:每个替换始终由两个模板字符串包围。
templateObject.length === substs.length + 1
如果替换在字面量中排在第一位,则在其前面加上一个空的模板字符串
> tagFunc`${'subst'}xyz`
{ templateObject: [ '', 'xyz' ], substs: [ 'subst' ] }
如果替换在字面量中排在最后一位,则在其后面加上一个空的模板字符串
> tagFunc`abc${'subst'}`
{ templateObject: [ 'abc', '' ], substs: [ 'subst' ] }
空的模板字面量生成一个模板字符串,并且没有替换
> tagFunc``
{ templateObject: [ '' ], substs: [] }
模板字符串有两种解释——已处理和原始。这些解释会影响转义
${ 前面的反斜杠 (\) 会阻止将其解释为替换的开头。标签函数 describe 允许我们探索这意味着什么。
function describe(tmplObj, ...substs) {
return {
Cooked: merge(tmplObj, substs),
Raw: merge(tmplObj.raw, substs),
};
}
function merge(tmplStrs, substs) {
// There is always at least one element in tmplStrs
let result = tmplStrs[0];
substs.forEach((subst, i) => {
result += String(subst);
result += tmplStrs[i+1];
});
return result;
}
让我们使用这个标签函数
> describe`${3+3}`
{ Cooked: '6', Raw: '6' }
> describe`\${3+3}`
{ Cooked: '${3+3}', Raw: '\\${3+3}' }
> describe`\\${3+3}`
{ Cooked: '\\6', Raw: '\\\\6' }
> describe`\``
{ Cooked: '`', Raw: '\\`' }
如您所见,只要已处理的解释中有替换或反引号,则原始解释中也会有。但是,字面量中的所有反斜杠都会出现在原始解释中。
反斜杠的其他出现解释如下
例如
> describe`\\`
{ Cooked: '\\', Raw: '\\\\' }
> describe`\n`
{ Cooked: '\n', Raw: '\\n' }
> describe`\u{58}`
{ Cooked: 'X', Raw: '\\u{58}' }
总而言之:反斜杠在原始模式下的唯一作用是转义替换和反引号。
String.raw 以下是您如何实现 String.raw
function raw(strs, ...substs) {
let result = strs.raw[0];
for (const [i,subst] of substs.entries()) {
result += subst;
result += strs.raw[i+1];
}
return result;
}
我之前演示了用于 HTML 模板化的标签函数 html
const tmpl = addrs => html`
<table>
${addrs.map(addr => html`
<tr><td>!${addr.first}</td></tr>
<tr><td>!${addr.last}</td></tr>
`)}
</table>
`;
const data = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
console.log(tmpl(data));
// Output:
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
如果在替换前面加上感叹号 (!${addr.first}),则它将进行 HTML 转义。标签函数检查替换前的文本以确定是否进行转义。
这是 html 的实现
function html(templateObject, ...substs) {
// Use raw template strings: we don’t want
// backslashes (\n etc.) to be interpreted
const raw = templateObject.raw;
let result = '';
substs.forEach((subst, i) => {
// Retrieve the template string preceding
// the current substitution
let lit = raw[i];
// In the example, map() returns an Array:
// If `subst` is an Array (and not a string),
// we turn it into a string
if (Array.isArray(subst)) {
subst = subst.join('');
}
// If the substitution is preceded by an exclamation
// mark, we escape special characters in it
if (lit.endsWith('!')) {
subst = htmlEscape(subst);
lit = lit.slice(0, -1);
}
result += lit;
result += subst;
});
// Take care of last template string
result += raw[raw.length-1]; // (A)
return result;
}
模板字符串始终比替换多一个,这就是为什么我们需要在 A 行中追加最后一个模板字符串的原因。
以下是 htmlEscape() 的简单实现。
function htmlEscape(str) {
return str.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`');
}
使用这种模板化方法,您可以做更多的事情
cond?then:else) 或逻辑或运算符 (||) 来完成 !${addr.first ? addr.first : '(No first name)'}
!${addr.first || '(No first name)'}
const theHtml = html`
<div>
Hello!
</div>`;
第一个非空白字符是 <div>,这意味着文本从第 4 列开始(最左边的列是第 0 列)。标签函数 html 可以自动删除所有前面的列。然后,前面的标记模板将等效于
const theHtml =
html`<div>
Hello!
</div>`;
// Without destructuring
${addrs.map((person) => html`
<tr><td>!${person.first}</td></tr>
<tr><td>!${person.last}</td></tr>
`)}
// With destructuring
${addrs.map(({first,last}) => html`
<tr><td>!${first}</td></tr>
<tr><td>!${last}</td></tr>
`)}
创建正则表达式实例有两种方法。
/^abc$/iRegExp 构造函数:new RegExp('^abc$', 'i')如果您使用后者,那是因为您必须等到运行时才能获得所有必要的成分。您正在通过连接三种片段来创建正则表达式
对于 #3,特殊字符(点、方括号等)必须进行转义,而 #1 和 #2 可以按字面使用。正则表达式标签函数 regex 可以帮助完成此任务
const INTEGER = /\d+/;
const decimalPoint = '.'; // locale-specific! E.g. ',' in Germany
const NUMBER = regex`${INTEGER}(${decimalPoint}${INTEGER})?`;
regex 如下所示
function regex(tmplObj, ...substs) {
// Static text: verbatim
let regexText = tmplObj.raw[0];
for ([i, subst] of substs.entries()) {
if (subst instanceof RegExp) {
// Dynamic regular expressions: verbatim
regexText += String(subst);
} else {
// Other dynamic data: escaped
regexText += quoteText(String(subst));
}
// Static text: verbatim
regexText += tmplObj.raw[i+1];
}
return new RegExp(regexText);
}
function quoteText(text) {
return text.replace(/[\\^$.*+?()[\]{}|=!<>:-]/g, '\\$&');
}
模板字面量和标记模板字面量是从 E 语言借用的,E 语言将此功能称为_准字面量_。
宏允许您实现具有自定义语法的语言结构。为语法像 JavaScript 这样复杂的编程语言提供宏是很困难的。该领域的研究正在进行中(请参阅 Mozilla 的 sweet.js)。
虽然宏在实现子语言方面比标记模板强大得多,但它们依赖于语言的标记化。因此,标记模板是互补的,因为它们专门用于文本内容。
如果我想从外部源(例如文件)加载模板字面量(例如 `Hello ${name}!`)怎么办?
如果你这样做,你就是在滥用模板字面量。鉴于模板字面量可以包含任意表达式并且是一个字面量,从其他地方加载它类似于加载表达式或字符串字面量——你必须使用 eval() 或类似的东西。
反引号是 JavaScript 中为数不多的尚未使用的 ASCII 字符之一。语法 ${} 用于插值非常常见(Unix shell 等)。
模板字面量这个术语在 ES6 规范制定过程中改的比较晚。以下是旧术语