传统的 JavaScript
语言,输出模板
通常是这样写的(下面使用了 jQuery
的方法)。
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
之前写法相当繁琐
不方便,ES6
引入了模板字符串
解决这个问题
。
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
模板字符串(template string)
是增加版
的字符串
,用反引号
作为标识
,它可以当作普通字符串
使用,也可以用来定义多行字符串
。
使用模板字符串
表示多行字符串
,所有的空格
和缩进
都会被保留在输出
之中。
(function (log, S) {
// 普通字符串
log(`In JavaScript '\n' is a line-feed.`);
// In JavaScript '
// ' is a line-feed.
// 多行字符串
log(`In JavaScript this is
not legal.`);
// In JavaScript this is
// not legal.
log(`string text line 1
string text line 2`);
// string text line 1
// string text line 2
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`);
})(console.log, String)
代码
中的模板字符串
,都是用反引号
表示。
如果在模板字符串
中需要使用反引号
,则前面要用反斜杠
转义。
(function (log, S) {
let greeting = `\`Yo\` World!`;
log(greeting);// `Yo` World!
})(console.log, String)
模板字符串
中嵌入变量
,需要将变量名
写在${}
之中。
(function (log, S) {
// 字符串中嵌入变量
let name = "Bob",
time = "today";
log(`Hello ${name}, how are you ${time}?`);
// Hello Bob, how are you today?
function authorize(user, action) {
if (!user.hasPrivilege(action)) {
throw new Error(
// 传统写法为
// 'User '
// + user.name
// + ' is not authorized to do '
// + action
// + '.'
`User ${user.name} is not authorized to do ${action}.`);
}
}
})(console.log, String)
简单表达式
模板字符串
中大括号
内部可以放入任意的 JavaScript 表达式
,可以进行运算
,以及引用对象属性
。
(function (log, S) {
let x = 1;
let y = 2;
log(`${x} + ${y} = ${x + y}`);
// "1 + 2 = 3"
log(`${x} + ${y * 2} = ${x + y * 2}`);
// "1 + 4 = 5"
let obj = {
x: 1,
y: 2
};
log(`${obj.x + obj.y}`);
// "3"
})(console.log, String)
调用函数
模板字符串
之中还能调用函数
。
(function (log, S) {
function fn() {
return "Hello World";
}
log(`foo ${fn()} bar`);
// foo Hello World bar
})(console.log, String)
字符串变量
由于模板字符串
的大括号内部
,就是执行 JavaScript 代码
,因此如果大括号内部
是一个字符串
,将会原样输出
。
(function (log, S) {
let msg = `Hello ${'Melon'}`;
log(msg);// Hello Melon
})(console.log, String)
需要时执行
如果需要引用模板字符串
本身,在需要
时执行
,可以像下面这样写。
(function (log, S) {
// 写法一
let str1 = 'return ' + '`Hello ${name}!`';
let func1 = new Function('name', str1);
log(func1('Melon')); // "Hello Melon!"
// 写法二
let str2 = '(name) => `Hello ${name}!`';
let func2 = eval.call(null, str2);
log(func2('Melon')); // "Hello Melon!"
})(console.log, String)
如果大括号
中的值不是字符串
,将按照一般的规则
转为字符串
。
比如,大括号
中是一个对象
,将默认调用对象
的toString
方法。
(function (log, S) {
let a = [1,3,4];
let o = {melon:1};
let d = Reflect.construct(Date,[]);
log(`${a} + ${o} = ${d}`);
// 1,3,4 + [object Object] = Mon Feb 11 2019 23:45:23 GMT+0800 (GMT+08:00)
})(console.log, String)
如果模板字符串
中的变量
没有声明
,将报错
。
(function (log, S) {
// 变量place没有声明
let msg = `Hello, ${place}`;// ReferenceError: place is not defined
})(console.log, String)
模板字符串
可以嵌套
。
下面tmpl
方法中,模板字符串
的变量
之中,又嵌入了另一个模板字符串
。
(function (log, S) {
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join('')}
</table>
`;
const data = [{
first: '<Jane>',
last: 'Bond'
},
{
first: 'Lars',
last: '<Croft>'
},
];
log(tmpl(data));
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
})(console.log, String)
const template = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
上面代码在模板字符串
之中,放置了一个常规模板
。
该模板使用<%...%>
放置 JavaScript
代码,使用<%= ... %>
输出 JavaScript
表达式。
将其转换为 JavaScript 表达式
字符串。
(function (log, S) {
function tmpl(data) {
const arr = [];
arr.push('<ul>');
for (let i = 0; i < data.supplies.length; i++) {
arr.push('\n\t<li>');
arr.push(data.supplies[i]);
arr.push('</li>');
};
arr.push('\n</ul>');
return arr.join('');
}
const data = {
supplies: ["broom", "mop", "cleaner"]
};
log(tmpl(data));
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>
})(console.log, String)
使用正则表达式
将模板中<%= ... %>
转换为echo
字符串拼接方法,然后再结合模板字符串
,转译最后的编译方法完整体
,最后再调用eval
动态执行函数编译
。
(function (log, S) {
function compile(template) {
const evalExpr = /<%=(.+?)%>/g;
const expr = /<%([\s\S]+?)%>/g;
template = template.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
template = template.replace(expr, '`); \n $1 \n echo(`');
template = 'echo(`' + template + '`);';
let script =
`(function parse(data){
let output = "";
function echo(html){
output += html;
}
${ template }
return output;
})`;
return script;
}
const data = {
supplies: ["broom", "mop", "cleaner"]
};
const temp = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
// let tmpl = new Function(temp,compile);
const compileStr = compile(temp);
log(compileStr);
// (function parse(data) {
// let output = "";
// function echo(html) {
// output += html;
// }
// echo(`
// <ul>
// `);
// for (let i = 0; i < data.supplies.length; i++) {
// echo(`
// <li>`);
// echo(data.supplies[i]);
// echo(`</li>
// `);
// }
// echo(`
// </ul>
// `);
// return output;
// })
let tmpl = eval(compileStr);
log(tmpl(data));
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>
})(console.log, String)
使用正则表达式
将模板中<%= ... %>
转换为arr.push
字符串拼接方法,然后再结合模板字符串
,转译最后的编译方法完整体
,最后再调用eval
动态执行函数编译
。
(function (log, S) {
function compile(template) {
const evalExpr = /<%=(.+?)%>/g;
const expr = /<%([\s\S]+?)%>/g;
template = template.replace(evalExpr, '`); \n arr.push( $1 ); \n arr.push(`');
// 替换模板字符串中的<%=...%>中的值
// 比如将 '<%= data.supplies[i] %>'
// 转换为 '`);\n arr.push( data.supplies[i]); \n arr.push(`'
template = template.replace(expr, '`); \n $1 \n arr.push(`');
// 替换模板字符串中的<%...%>中的值
// 比如将 '<% for(let i=0; i < data.supplies.length; i++) { %>'
// 转换为 '`);\n for(let i=0; i < data.supplies.length; i++) \n arr.push(`'
template = 'arr.push(`' + template + '`);'; // 做最外层的包裹
let script =
`(function parse(data){
const arr = [];
${ template }
return arr.join('');
})`;
return script;
}
const data = {
supplies: ["broom", "mop", "cleaner"]
};
const temp = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
// let tmpl = new Function(temp,compile);
const compileStr = compile(temp);
log(compileStr);
// (function parse(data){
// const arr = [];
// arr.push(`
// <ul>
// `);
// for(let i=0; i < data.supplies.length; i++) {
// arr.push(`
// <li>`);
// arr.push( data.supplies[i] );
// arr.push(`</li>
// `);
// }
// arr.push(`
// </ul>
// `);
// return arr.join('');
// })
let tmpl = eval(compileStr);
log(tmpl(data));
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>
})(console.log, String)
前面提到标签模板
里面,可以内嵌其他语言
。
但是,模板字符串
默认会将字符串转义
,导致无法嵌入其他语言
。
举例来说,标签模板里面可以嵌入 LaTEX
语言。
function latex(strings) {
// ...
}
let document = latex`
\newcommand{\fun}{\textbf{Fun!}} // 正常工作
\newcommand{\unicode}{\textbf{Unicode!}} // 报错
\newcommand{\xerxes}{\textbf{King!}} // 报错
Breve over the h goes \u{h}ere // 报错
`
上面代码中,变量document
内嵌的模板字符串
,对于 LaTEX
语言来说完全是合法
的。
但是 JavaScript
引擎会报错。原因就在于字符串
的转义
。
模板字符串
会将\u00FF
和\u{42}
当作 Unicode
字符进行转义,所以\unicode
解析时报错;
而\x56
会被当作十六进制
字符串转义,所以\xerxes
会报错。
也就是说,\u
和\x
在 LaTEX
里面有特殊含义
,但是 JavaScript
将它们转义了。
为了解决这个问题,ES2018
放松了对标签模板
里面的字符串转义
的限制。
如果遇到不合法
的字符串转义
,就返回undefined
,而不是报错,并且从raw
属性上面可以得到原始字符串
。
function tag(strs) {
strs[0] === undefined
strs.raw[0] === "\\unicode and \\u{55}";
}
tag`\unicode and \u{55}`
上面代码中,模板字符串
原本是应该报错的,但是由于放松
了对字符串转义
的限制
,所以不报错了,JavaScript
引擎将第一个字符设置为undefined
/
但是raw
属性依然可以得到原始字符串
,因此tag
函数还是可以对原字符串
进行处理。
注意,这种对字符串转义
的放松
,只在标签模板解析
字符串时生效,不是标签模板
的场合,依然会报错
。
let bad = `bad escape sequence: \unicode`; // 报错