模板字符串
的功能,不仅仅是上面这些。它可以紧跟在一个函数名
后面,该函数将被调用来处理这个模板字符串
。这被称为标签模板
功能(tagged template
)。
(function (log, S) {
log `123`;// [ '123' ]
// 等同于
log(123);// 123
})(console.log, String)
标签模板
其实不是模板
,而是函数调用
的一种特殊形式
。标签
指的就是函数
,紧跟在后面的模板字符串
就是它的参数
。
模板处理函数
的第一个参数
(模板字符串
数组),还有一个raw
属性。
(function (log, S) {
log `123`;// ["123", raw: Array[1]]
// 等同于
log(123);// 123
})(console.log, String)
上面代码中,console.log
接受的参数,实际上是一个数组
。
该数组
有一个raw属性
,保存的是转义
后的原字符串
。
请看下面的例子。
(function (log, S) {
function tag(strings) {
const currRaw = strings.raw;
log(strings);
log(currRaw);
log(typeof currRaw);
for(const item of currRaw){
log(item);
}
}
tag`First line\nSecond line`;
// [ 'First line\nSecond line' ]
// [ 'First line\\nSecond line' ]
// object
// strings.raw[0] 为 "First line\\nSecond line"
// First line
// Second line
// tag('First line\nSecond line');
// undefined
// undefined
// TypeError: currRaw is not iterable
})(console.log, String)
上面代码中,tag
函数的第一个参数strings
,有一个raw
属性,也指向一个数组
。
该数组
的成员
与strings数组
完全一致。
比如,strings数组
是["First line\nSecond line"]
,那么strings.raw
数组就是["First line\\nSecond line"]
。
两者唯一的区别,就是字符串
里面的斜杠
都被转义
了。
比如,strings.raw
数组会将\n
视为\\
和n
两个字符
,而不是换行符
。
这是为了方便取得转义
之前的原始模板
而设计
的。
但是,如果模板字符
里面有变量
,就不是简单的调用
了,而是会将模板字符串
先处理成多个参数
,再调用函数
。
(function (log, S) {
function tag(strings) {
const currRaw = strings.raw;
log(strings);
log(currRaw);
log(typeof currRaw);
for(const item of currRaw){
log(item);
}
}
const a = 5;
const b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// [ 'Hello ', ' world ', '' ]
// object
// Hello
// world
})(console.log, String)
上面代码中,模板字符串前面有一个标识名tag
,它是一个函数
。
tag
函数的其他参数
,都是模板字符串
各个变量被替换
后的值
。
const a = 5;
const b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
由于上面案例中,模板字符串含有两个变量
,因此tag
会接受到value1
和value2
两个参数
。
tag
函数所有参数
的实际值
如下。
参数顺序标识 | 参数值说明 |
---|---|
第一个参数 | ['Hello ', ' world ', ''] |
第二个参数 | 15 |
第三个参数 | 50 |
也就是说,tag
函数实际上以下面的形式调用。
(function (log, S) {
function tag(strings) {
const currRaw = strings.raw;
log(strings);
log(currRaw);
log(typeof currRaw);
for(const item of currRaw){
log(item);
}
}
const a = 5;
const b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// [ 'Hello ', ' world ', '' ]
// object
// Hello
// world
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
// [ 'Hello ', ' world ', '' ]
// undefined
// undefined
// TypeError: currRaw is not iterable
})(console.log, String)
整个表达式
的返回值
,就是tag
函数处理模板字符串
后的返回值
。
函数tag
依次会接收到多个参数
。
function tag(stringArr, value1, value2){
// ...
}
// 等同于
function tag(stringArr, ...values){
// ...
}
tag
函数的第一个参数
是一个数组
,该数组
的成员
是模板字符串
中那些没有变量替换
的部分。
也就是说,变量替换
只发生在数组
的第一个成员
与第二个成员
之间、第二个成员
与第三个成员
之间,以此类推。
我们可以按照需要编写tag
函数的代码。下面是tag
函数的一种写法,以及运行结果。
(function (log, S) {
const a = 5;
const b = 10;
function tag(s, v1, v2) {
log(s[0]);
log(s[1]);
log(s[2]);
log(v1);
log(v2);
return "OK";
}
log(tag `Hello ${ a + b } world ${ a * b}`);
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"
})(console.log, String)
下面是一个更复杂
的例子。
(function (log, S) {
// 注意不要使用箭头函数,否则会导致结果会是
// The total is function String() { [native code] } ( with tax)
// const passthru = (literals)=>{
const passthru = function(literals){
let result = '';
let i = 0;
log(...literals);
while (i < literals.length) {
result += literals[i++];
if (i < arguments.length) {
result += arguments[i];
}
}
return result;
}
const total = 30;
const msg = passthru `The total is ${total} (${total*1.05} with tax)`;
log(msg);
// The total is ( with tax)
// "The total is 30 (31.5 with tax)"
})(console.log, String)
上面这个例子
展示了,如何将各个参数
按照原来的位置拼合
回去。
passthru
函数采用 rest
参数的写法如下。
function passthru(literals, ...values) {
let output = "";
let index;
for (index = 0; index < values.length; index++) {
output += literals[index] + values[index];
}
output += literals[index]
return output;
}
标签模板
的一个重要应用,就是过滤 HTML
字符串,防止用户输入恶意内容。
const SaferHTML = function (templateData) {
let s = templateData[0];
for (let i = 1; i < arguments.length; i++) {
const arg = String(arguments[i]);
// Escape special characters in the substitution.
s += arg.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
const message =
SaferHTML `<p>${sender} has sent you a message.</p>`;
上面代码中,sender
变量往往是用户提供的,经过SaferHTML
函数处理,里面的特殊字符都会被转义。
(function (log, S) {
const SaferHTML = function (templateData) {
let s = templateData[0];
for (let i = 1; i < arguments.length; i++) {
const arg = String(arguments[i]);
// Escape special characters in the substitution.
s += arg.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
const sender = '<script>alert("abc")</script>'; // 恶意代码
const message =
SaferHTML `<p>${sender} has sent you a message.</p>`;
log(message);// <p><script>alert("abc")</script> has sent you a message.</p>
})(console.log, String)
标签模板
的另一个应用,就是多语言
转换(国际化处理)。
i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
// "欢迎访问xxx,您是第xxxx位访问者!"
模板字符串本身并不能取代 Mustache
之类的模板库,因为没有条件判断
和循环
处理功能,但是通过标签函数
,你可以自己添加这些功能。
// 下面的hashTemplate函数
// 是一个自定义的模板处理函数
const libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
#end
</ul>
`;
除此之外,你甚至可以使用标签模板
,在 JavaScript
语言之中嵌入其他语言
。
jsx`
<div>
<input
ref='input'
onChange='${this.handleChange}'
defaultValue='${this.state.value}' />
${this.state.value}
</div>
`
上面的代码通过jsx
函数,将一个 DOM
字符串转为 React
对象。你可以在 GitHub
找到jsx
函数的具体实现。
下面则是一个假想的例子,通过java
函数,在 JavaScript
代码之中运行 Java
代码。
java`
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello World!"); // Display the string.
}
}
`
HelloWorldApp.main();