\n
)String.raw
ES6 有两种新的字面量:模板字面量 和 带标签的模板字面量。这两种字面量名称相似,外观也相似,但它们却截然不同。因此,区分以下几点非常重要
模板字面量 是可以跨越多行并包含插值表达式(通过 ${···}
插入)的字符串字面量
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$/i
RegExp
构造函数: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 规范制定过程中改的比较晚。以下是旧术语