不写分号的前端攻城师们注意了!需要加分号的情况又多了一种

本来只想提醒一下然后顺便介绍一下 Tagged Template,没想到引起了一起圣战,我真的不是故意的。。。

==============以下正文==============


前段时间上课的时候,用到了 ES6 的模板字符串,大概写了下面这样一段代码:

var c = 5
var factor = 30
card.style.transform = `perspective(1000px) rotateX(${-c/factor}deg)`

因为赋值字符串比较复杂,为了方便,我就用了 ES6 里的模板字符串。

然而有一会儿我又想不给这个属性赋这个字符串,同时又想调试的时候方便的查看这个字符串以确认我是不是拼错了,于是我把代码改成了下面这样:

var c = 5
var factor = 30
card.style.transform = 
`perspective(1000px) rotateX(${-c/factor}deg)`

注意区别,即让 card.style.transform = 独自成一行,然后我视情况把这一行给注释掉,这样就可以在不赋值的同时又可以在调试的时候选中下面一行的模板字符串然后 hover 上去看到字符串的值,就像下面这个图里一样

然而在我把等号那一行注释掉的时候,代码竟然报错了,而且错误很奇怪:TypeError: 30 is not a function.

唉嘿?30 为什么会被当函数调用?

我一开始以为是注释那一行的问题,然后就把注释的那一行整个删掉了,然而错误还是会出现(很显然啊)。

然后我尝试给模板字符串那一行加上一个前置分号,这下错误就没有了。

于是乎问题明白了:其实是这两行被连在一起执行了。可是问题还没完,为什么连在一起执行会把前面一行的 30 当成函数呢?既然如此,那我就把 30 改成一个函数,看看函数会接收到什么参数好了,于是我把代码改成下面这样:

var x = 42
var f = function() {
  console.log(arguments)
}

`foo${x}bar\\`

控制台确实 log 出了一个 arguments 对象,说明函数确实被调用了,打出的参数中,第一个实参是一个数组,内容为 ['foo', 'bar'],第二个是 x 的值,即 8。

进一步展开第一个参数的内容,看到它虽然是一个数组,但还有额外的一个 raw 属性,依然指向一个数组,也是 ['foo', 'bar']。

(看到 raw 让我想起了 Python 里的原始字符串。)

即函数 f 是以如下形式被调用的:

var firstArg = ['foo', 'bar']
firstArg.raw = ['foo', 'bar']

f(firstArg, 8)

如下图所示:


什么情况,为什么把函数跟模板字符串连着写会让函数运行?而且还会给函数传入了特定的参数,这肯定是 ES6 中新的语法!而且这几句代码的执行结果也不是最后一句表达式的值,而是 undefined。而且 raw 数组里为什么会显示两个反斜杠,这其中必有蹊跷。

(另外这让我想起了 2010 年左右的时候,字符串后面紧跟一个正则表达式相当用调用字符串上的 match 方法并传入那个紧跟着的正则表达式,但后来这个语法又不能用了。我一直很奇怪到底是什么情况,有机会我一定要问一下贺老师,谁帮我爱特一下~~)

去年看《Understanding ES6》时,隐约记得 ES6 中增加了一个类似 Python 的 r'foobar' 这样的字符串表达方式,可以让字符串的内容就为书写时的内容而忽略转义。好像是这么书写:“raw反引号foobar反引号”(反引号没有全角的,只能用文字替代了)。

于是我决定查文档。

最终在 Understanding ES6 中翻出了相应的章节(这时我才想起来,去年读《Understanding ES6》的时候,这本书还没写完,当时并没有这一节):

> A template tag performs a transformation on the template literal and returns the final string value.

原来,在一个函数后面紧跟一个模板字符串是 ES6 中的新语法,叫做“tagged template”,形如以下(其中 tag 是一个函数):

tag`the string${a} to${b} be tagged`

如果这么写,模板字符串并不会直接被插值,而是会被拆成各个部分然后把各个部分按一定形式传给这个 tag 的函数。而根据 ASI 的规则,文章开头的代码中,并不会在函数与模板字符串之间插入一个分号,而模板字符串的前面的表达式总会被认为是一个函数,这就导致如果模板字符串的左边不是函数的话,就会报 TypeError 错误。

所以最终的结论就是:原来需要加分号的 5 种情况(+,-,[,(,/)又加了一种,即模板字符串如果出现在行首,其(即反引号:“`”)前面也要加上分号(或者在其前一行的末尾加上分号)

为了确认这一点,我用 Babel 分别编译了下面两段代码:

var a = function(){}

`foobar`
var a = function(){};

`foobar`;

得到的结果分别如下(注意分号):


说明,这两种情况确实会被引擎理解为不一样的语义。

下面,来解释一下 Tagged Template 的用法吧,随便 YY 了一下发现这玩意可以玩出很多花样。

首先,Tag 函数的第一个参数是一个数组,数组的各项是模板字符串的被插值符号分成的各个部分,各部分的内容是已经转义过后的值。同时,这第一个数组参数还有一个 raw 属性,也是个数组,它的各项也是模板字符串的各部分,但是,是未被转义的内容,即反斜杠会被保留!Tag 函数从第二个参数开始,分别是每个插值部分表达式的计算结果。Tag 函数的返回值会做为这个 Tagged Template 的结果。

举个例子:

var a = 2
var b = 4

function t(){

}

t`abc\\def${a}foo\nbar${a*b}`

在上例中,tag 函数 t 将会接收到 3 个参数,第一个为一个数组,第二个为 2,即表达式 a 的计算结果,第三个为 8,即表达式 a * b 的计算结果。

而第一个参数指向的数组有两项,内容为【abc\def】(注意反斜杠为字符串内容的一部分)和【foo回车bar】而这个数组有一个raw属性,其值为【abc\\def】和【foo\nbar】,注意全角引号内所看到的即为字符串的内容,反斜杠部分被保留下来了(因为知乎专栏的编辑器也会转义,所以我用了全角斜杠表示)。

那么,这玩意儿能怎么玩呢?

首先,如果你有一个函数接收一个字符串做为参数,那么写成 tagged string 是可以变的很简单的,因为使用 tagged string 你可以拿到字符串转义之前的内容:

var raw = function(parts){
  return parts.raw.join('')
}

上面的 raw 函数没有处理插值的情况,会忽略掉所有插值的部分。其实 String 上已经有了一个这样功能的函数了:【String.raw反引号\\\\反引号】将得到内容为 4 个反斜杠的字符串,并且能够处理插值的情况。

使用正则表达式构造函数创建正则对象就可以这么写了:

new RegExp(String.raw`\d+`, 'gi')

而不用像以前需要多重转义:

new RegExp('\\d+', 'gi')

当然,上面的写法好像有点脱裤子放屁,因为在JS里可以写成正则直接量。但如果你想要通过字符串来创建其它的对象,则使用这种写法会简便很多。

然而需要注意的是 String.raw\\\ 是不行的,因为这么写的话最后一个反引号在写的时候就被反斜杠转义了,会报语法错误,所以反斜杠连续出现的时候必须出现偶数次。

另一点,模板字符串的插值部分是可以继续出现模板字符串的,只要嵌套正确,是不会报语法错误并且也没有歧义的:

var s = `
  <div>
    ${
      `abc` + `def`
    }
  </div>
`

那么它还可以有什么玩法呢?

比如说可以在 js 里更方便的表达 html,加上合适的语法高亮,是可以很简洁的,此处只谈脑洞,不考虑可行性:

function p(literals){
  //此处实现忽略
}

function a(literials) {
  return '<a>' + literials.join('') + '</a>'
}

document.body.innerHTML = p`
The quick brown fox ${a`jumps`} over the lazy dog.
`

从使用上讲,你可以把一对反引号当成是以另一种形式的函数调用,然后我们又可以方便的拿到代码中书写的值(转义和非转义的),理论上甚至可以做到不编译就实现 React 的 jsx 语法(性能当然不如编译好了,但性能和可行性暂且不论):

var Button = React.CreateClass()
var btnText = '提交'

//注意下面一行的模板字符串会先与左边的 render 函数结合
//所以 to 函数是 render 返回的对象上的方法。
//另外 to 方法是我 YY 出来的
ReactDOM.render`<Button text=${btnText}/>`.to(document.body)

//你可能会认为这上面这种写法跟下面没什么区别
//那请考虑一下下面的代码如何拿到当前作用域内的 btnText 变量的值
ReactDOM.render('<Button text={btnText}/>').to(document.body)

总之,可以很方便的实现自己的 DSL。

以后有脑洞了我再补上来~

最后,虽然需要写分号的情况多了一种,但这种也像其它几个符号一样,并不常见,所以也不用太在意,只要在出现问题的时候知道是怎么回事就好。

编辑于 2016-12-29

文章被以下专栏收录