详解JavaScript ES6中的模板字符串


Posted in Javascript onJuly 28, 2015

在 ES6 中引入了一种新的字符串字面量 — 模板字符串,除了使用反引号 (`) 表示,它们看上去和普通的字符串没有什么区别。在最简单的情况下,他们就是普通的字符串:

context.fillText(`Ceci n'est pas une cha?ne.`, x, y);
 
context.fillText(`Ceci n'est pas une cha?ne.`, x, y);

之所以被称为模板字符串,是因为模板字符串为 JS 引入了简单的字符串插值特性,也就是说,可以方便优雅地将 JS 的值插入到字符串中。

很多地方可以用到模板字符串,看下面这个不起眼的错误提示消息:

function authorize(user, action) {
 if (!user.hasPrivilege(action)) {
  throw new Error(
   `User ${user.name} is not authorized to do ${action}.`);
 }
}
 
function authorize(user, action) {
 if (!user.hasPrivilege(action)) {
  throw new Error(
   `User ${user.name} is not authorized to do ${action}.`);
 }
}

上面代码中,${user.name} 和 ${action} 被称为模板占位符,JavaScript 将把 user.name和 action 的值分别插到对应的位置上,然后生成像这样 “User jorendorff is not authorized to do hockey.” 的字符串。

现在,我们看到了一个比 + 运算符更优雅的语法,下面是一些你期待的特性:

    模板占位符可以是任何 JavaScript 表达式,所以函数调用和四则运算等都是合法的。(甚至你还可以在一个模板字符串中嵌套另一个模板字符串。)
    如果一个值不是字符串,它将被转换为字符串。例如,如果 action 是一个对象,那么该对象的 .toString() 将被调用,来将其转换为字符串。
    如果你想在模板字符串中使用反引号,你需要使用反斜杠 \ 将其转义。
    同样地,如果想在模板字符串中输出 ${,也需要使用反斜杠将其转义:\${ 或 $\{。
    模板字符串可以跨越多行:

$("#warning").html(`
 <h1>Watch out!</h1>
 <p>Unauthorized hockeying can result in penalties
 of up to ${maxPenalty} minutes.</p>
`);
 
$("#warning").html(`
 <h1>Watch out!</h1>
 <p>Unauthorized hockeying can result in penalties
 of up to ${maxPenalty} minutes.</p>
`);

    模板字符串中所有的空格、换行和缩进,都将被原样输出到结果字符串中。

下面我们来看看模板字符串做不到的事情:

    不会自动转义特殊字符,为了避免跨站脚本漏洞,你还是需要小心对待不可信的数据,这一点上与普通字符串一样。
    不能与国际化库配合使用,不处理特殊语言格式的数字、日期等。
    不是模板引擎(比如 Mustache 或 Nunjucks)的替代品。模板字符串没有处理循环的语法 — 不能通过一个数组构建出一个表格(table)。

为了解决这些限制,ES6 为开发者和库设计者提供了另一种模板字符串 — 标签模板。

标签模板的语法很简单,只需要在开始的反引号前引入一个标签。看第一个例子:SaferHTML,我们要使用这个标签模板来解决上述的第一个限制:自动转义特殊字符。

需要注意的是,SaferHTML 方法并不是 ES6 标准库提供的,我们需要自己来实现:

var message =
 SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`;
 
var message =
 SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`;

这里的 SaferHTML 标签是单个标识符,标签也可以是属性,比如 SaferHTML.escape,甚至还可以是方法调用:SaferHTML.escape({unicodeControlCharacters: false})。准确地说,任何 ES6 的成员表达式或调用表达式都可以作为标签。

可以看出,模板字符串仅仅是字符串连接的语法糖,而标签模板确是一个完全不同的东西:函数调用。

所以,上面代码等价于:

var message =
 SaferHTML(templateData, bonk.sender);
 
var message =
 SaferHTML(templateData, bonk.sender);

其中 templateData 是一个不可变的字符串数组,由 JS 引擎基于源模板字符串生成,这里的数组含有两个元素,因为模板字符串被占位符分隔后含有两个字符串,因此,templateData 将是这样: Object.freeze(["<p>", " has sent you a bonk.</p>"]

(事实上,templateData 上还有另一个属性:templateData.raw,本文并深入不讨论该属性。该属性的值也是一个数组,包含了标签模板中所有的字符串部分,但字符串中包含了转义序列,看上去更像源代码中的字符串,比如 \n。ES6 的内置标签 String.raw 将使用这些字符串。)

这就使得 SaferHTML 方法可以随意解析这两个字符串,存在 N 中替换方式。

在继续阅读钱,你可能在苦苦思索如何实现 SaferHTML 方法。

下面是一种实现(gist):

function SaferHTML(templateData) {
 var s = templateData[0];
 for (var i = 1; i < arguments.length; i++) {
  var 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;
}
 
function SaferHTML(templateData) {
 var s = templateData[0];
 for (var i = 1; i < arguments.length; i++) {
  var 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;
}

有了上面的方法,即使使用一个恶意的用户名,用户也是安全的。

一个简单的例子并不足以说明标签模板的灵活性,让我们重温一下上面列举的模板字符串的限制,看看我们还可以做些什么。

    模板字符串不会自动转义特殊字符,但是我们可以通过标签模板来解决这个问题,事实上我们还可以将 SaferHTML 这个方法写的更好。从安全角度来看,这个 SaferHTML 非常脆弱。在 HTML 中,不同的地方需要用不同的方式去转义,SaferHTML 并没有做到。稍加思考,我们就可以实现一个更加灵活的 SaferHTML方法,能够将 templateData 中的任何一个 HTML 转义,知道哪个占位符是纯 HTML;哪个是元素的属性,从而需要对 ' 和 " 转义;哪个是 URL 的 query 字符串,从而需要用 URL 的 escaping 方法,而不是 HTML 的 escaping;等等。这似乎有些牵强,因为 HTML 转义效率比较低。辛运是的,标签模板的字符串是保持不变的,SaferHTML 可以缓存已经转义过的字符串,从而提高效率。
    模板字符串并没有内置的国际化特性,但通过标签模板,我们可以添加该特性。Jack Hsu 的文章详细介绍了实现过程,看下面例子:

i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
// => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.




i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
// => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.

上面例子中的 name 和 amount 很好理解,将被 JS 引擎替换为对应的字符串,但是还有一个没有见过的占位符::c(CAD),这将被 i18n 标签处理,从 i18n 的文档可知::c(CAD)表示 amount 是加拿大美元货币值。

    模板字符串不能替代 Mustache 和 Nunjucks 这类模板引擎,部分原因在于模板字符串不支持循环和条件语句。我们可以编写一个标签来实现这类功能:

// Purely hypothetical template language based on
// ES6 tagged templates.
var libraryHtml = hashTemplate`
 <ul>
  #for book in ${myBooks}
   <li><i>#{book.title}</i> by #{book.author}</li>
  #end
 </ul>
`;

 
// Purely hypothetical template language based on
// ES6 tagged templates.
var libraryHtml = hashTemplate`
 <ul>
  #for book in ${myBooks}
   <li><i>#{book.title}</i> by #{book.author}</li>
  #end
 </ul>
`;

灵活性还不止于此,需要注意的是,标签函数的参数不会自动转换为字符串,参数可以是任何类型,返回值也一样。标签模板甚至可以不需要字符串,你可以使用自定义标签来创建正则表达式、DOM 树、图片、代表整个异步进程的 Promise、JS 数据结构、GL 着色器…

标签模板允许库设计者创建强大的领域特定语言。这些语言可能看上去并不像 JS,但他们可以无缝嵌入到 JS 中,并且可以与语言的其余部分进行交互。顺便说一下,我还没有在其他语言中见过类似的特性,我不知道这个特性讲给我们带来些什么,但各种可能性还是非常令人兴奋的。

Javascript 相关文章推荐
JavaScript高级程序设计(第3版)学习笔记11 内建js对象
Oct 11 Javascript
js 自动播放的实例代码
Nov 19 Javascript
jQuery晃动层特效实现方法
Mar 09 Javascript
javascript实现自动输出文本(打字特效)
Aug 27 Javascript
浅析javascript的return语句
Dec 15 Javascript
分享我对JS插件开发的一些感想和心得
Feb 04 Javascript
Angular 根据 service 的状态更新 directive
Apr 03 Javascript
AngularJS入门教程之AngularJS指令
Apr 18 Javascript
微信小程序 加载 app-service.js 错误解决方法
Oct 12 Javascript
vue-router 路由基础的详解
Oct 17 Javascript
微信小程序实现单选功能
Oct 30 Javascript
angular4实现带搜索的下拉框
Mar 25 Javascript
javascript解决IE6下hover问题的方法
Jul 28 #Javascript
JavaScript如何自定义trim方法
Jul 28 #Javascript
详解JavaScript ES6中的Generator
Jul 28 #Javascript
深入解读JavaScript中的Iterator和for-of循环
Jul 28 #Javascript
JavaScript调用客户端Java程序的方法
Jul 27 #Javascript
详细解读JavaScript编程中的Promise使用
Jul 27 #Javascript
JavaScript中的Repaint和Reflow用法详解
Jul 27 #Javascript
You might like
PHPMailer邮件类利用smtp.163.com发送邮件方法
2008/09/11 PHP
mysql limit查询优化分析
2008/11/12 PHP
php使用curl抓取qq空间的访客信息示例
2014/02/28 PHP
PHP图片处理之图片旋转和图片翻转实例
2014/11/19 PHP
PHP程序员不应该忽略的3点
2015/10/09 PHP
PHP使用Redis长连接的方法详解
2018/02/12 PHP
PHP微信发送推送消息乱码的解决方法
2019/02/28 PHP
仿163填写邮件地址自动显示下拉(无优化)
2008/11/05 Javascript
Extjs 3.3切换tab隐藏相应工具栏出现空白解决
2013/04/02 Javascript
使用JS取得焦点(focus)元素代码
2014/03/22 Javascript
JavaScript通过字典进行字符串翻译转换的方法
2015/03/19 Javascript
jQuery检测输入的字符串包含的中英文的数量
2015/04/17 Javascript
JS实现点击按钮后框架内载入不同网页的方法
2015/05/05 Javascript
详细解读AngularJS中的表单验证编程
2015/06/19 Javascript
谈一谈JS消息机制和事件机制的理解
2016/04/14 Javascript
原生javascript实现分页效果
2017/04/21 Javascript
基于vue中css预加载使用sass的配置方式详解
2018/03/13 Javascript
node.js自动上传ftp的脚本分享
2018/06/16 Javascript
js HTML DOM EventListener功能与用法实例分析
2020/04/27 Javascript
Python中变量交换的例子
2014/08/25 Python
python实现将文件夹下面的不是以py文件结尾的文件都过滤掉的方法
2018/10/21 Python
python实现手机销售管理系统
2019/03/19 Python
Python内置random模块生成随机数的方法
2019/05/31 Python
python画图——实现在图上标注上具体数值的方法
2019/07/08 Python
python 计算两个列表的相关系数的实现
2019/08/29 Python
python中sklearn的pipeline模块实例详解
2020/05/21 Python
python能做哪些生活有趣的事情
2020/09/09 Python
Python字节单位转换(将字节转换为K M G T)
2021/03/02 Python
canvas之万花筒效果的简单实现(推荐)
2016/08/16 HTML / CSS
C++如何引用一个已经定义过的全局变量
2014/08/25 面试题
27个经典Linux面试题及答案,你知道几个?
2013/01/10 面试题
2014小学植树节活动总结
2014/03/10 职场文书
工作评语大全
2014/04/26 职场文书
国庆促销活动总结
2014/08/29 职场文书
深度学习小工程练习之垃圾分类详解
2021/04/14 Python
Nginx隐藏式跳转(浏览器URL跳转后保持不变)
2022/04/07 Servers