在讲 ES 2015 新语法之前,先来说一下为什么叫 ES。JavaScript 是这门语言的名称,它有一个为它制定标准化的组织 European Computer Manufacturers Association,直译就是欧洲计算机制造商协会。这个 ECMA 制定的 JavaScript 的实现标准,被称为 ECMAScript,不同组织写出来的 JavaScript 语言都要遵守这个 ECMAScript 标准,所以就简写为 ES+版本号。

这套 ES 标准在2015年之前最高的版本是5.1,也是唯一一个需要大家注意的版本:尽管各个流浏览器实现的 ES 新语法都有差异,但5.1版本是目前各大主流浏览器都支持的。这里再介绍一下传说中的 Babel.js 项目,它的目的很简单,把新语法翻译成老标准的语法。最主要的应用,就是把你写的 ES 201x 的语法翻译成 ES5.1,这样各个浏览器都是支持的。

在2015年的时候,ES6 正式发布,也很自然按照年份被称为 ES 2015。之后 ES 每年都发布一个版本,今年也就是2018年的草案也已经在年初发布了,从6开始今年该到版本9了但是不好记,所以大家都喜欢用 201x 来称呼。虽然语法是归在不同版本内的,但是各家 JS 的实现进度不同。你问一个语法具体是哪个版本实现的,估计10个人里有9个说不清楚,反正不是5.1的语法,所以大家也不区分了,直接都叫 ES 201x。

已经出来了好几年了,所以各种介绍的文章也很全面,我就不复制粘贴又臭又长了。在这里向大家推荐几个学习语法的英文网站,我就是从 babel 和 es6-features.org 这两个网站学习的:

https://babeljs.io/learn-es2015/

http://es6-features.org/

https://css-tricks.com/lets-learn-es2015/

中文的话,可以看这个网站:

http://es6.ruanyifeng.com

另外呢,MDN( https://developer.mozilla.org ) 上对于这些语法的介绍是很全面的,中文英文也都有。

 

那么接下来呢,我打算讲一讲这些新语法都解决了哪些问题,你应该怎么使用它,这是一般介绍语法的文章不会介绍的。提前说明一下,我个人平时都在用 airbnb 的代码风格,所以这些代码都会延用它家的风格来写。

一、const 与 let

首先要说的是,由于 var 的问题很多,新语法中永远不要用 var。它有的主要问题作用域是整个函数(function)而不是语法块(block),导致的最大问题是会让 closure 出现“bug”。而且这个“bug”还很经典,出现在许多著名语言中,其中也包括了

脚本界的老二 Python。构造这个 bug 的方法很简单:

1 {
2   const funcs = [];
3   for (var i = 0; i < 5; i++) {
4     funcs.push(() => i);
5   }
6   funcs.map(fn => fn());
7 }

把上面的代码粘贴到浏览器的 console 里面,结果是5个5,而不是 [0, 1, 2, 3, 4],惊不惊喜,意不意外?虽然把上面的 var 换成 let 就成功的解决了这个问题,但按照现在 JS 倾侧 FP 而不鼓励使用 for 的趋势,下面的方法才是正确的使用姿势:

1 import { times } from 'lodash';
2 
3 const funcs = times(5, i => () => i);

lodash 是一个著名而且非常流行的 JS 库,以后会有专门的文章介绍它。下面再给出其它语言生成这个 clusure 数组的正确投资,用 Python 举例:

1 funcs = []
2 for i in range(5):
3   funcs.append((lambda x: lambda: x)(i))
4 [fn() for fn in funcs]

再嵌一层函数来返回需要生成函数的函数,然后再把参数传过去。

那么 const 呢,我在另外一个扯一扯编译语言的系列中已经提过,能用 const 就不要用 let。如果你使用了 eslint 的话,只赋值过一次的 let 变量会被提示应该换成 const。

总结下来,就是用 const 代替 var,const 实在解决不了的情况,再用 let。不要觉得 const 多敲俩字母就不爱用,早几年还能看到 let 满天飞没有 const 的项目,现在一个项目里 let 出现的少才说明作者的水平高。一旦用了 var 你就是菜B一个,所以千万不要用。

二、=> 箭头函数(Arrow Function)

简单来讲是 function () {} 的简写形式,括号和参数列表写在 => 前面,大括号和函数体写在 => 后面,但是有3个非常重要的区别:

  1. function 的形式有两组重要的变量,一个是 arguments,主要代表一个包含所有实参的数组,另外一组是对象的上下文环境,主要包括 this、super。箭头函数本身是没有 this、super、arguments 这几个变量的。
  2. 如果箭头函数的函数体只返回一个表达式,也就是 () => { return EXPRESSION; },可以省略 { return },也就变成了 () => EXPRESSION
  3. 当箭头函数的参数只有一个时,可以省略参数两边的括号。上面那段代码中,就是因为外层的函数只有一个参数,我把 (i) => () => i 简写成了上面的形式。

最后一点并不重要,但是 airbnb 的代码风格要求在只有一个参数时不使用括号,所以我在这里专门提到了,但前面两点都有值得说的地方。

第一条中不再带 this 等上下文变量,是为了解决之前常常被诟病的搞不清当前 this 是啥的问题。JS 的对象模型是非主流的 prototype(原型)模型,在调用对象函数时,实际上使用的是类似 func.apply(this, arguments) 或者 func.call(this, ...arguments) 来实现的。所以当对象成员函数里面又套了一层 function 的时候,由于在内层函数声明的时候 this 并没有关联任何上下文变量,而导致在内层函数中 this 使用时并不是该对象,经常会导致试图调用 undefined 的成员函数的问题。而且因为这个模型实在是太非主流了,所以大家经常会忘掉甚至搞不清。所以这个语法在很大程度上就是为了解决 this 的问题。

第二条也会带来一个问题,很多时候我们想返回一个 Object,比如 { foo: 123, bar: 'baz' },但是 {} 这个东西又是函数体。很遗憾编译器没那么聪明,当你写 () => { foo: 123, bar: 'baz' } 的时候,会报语法错误。解决的办法也很简单,() 虽然套在表达式的外面它还是一个表达式,所以 () => ({ foo: 123, bar: 'baz' }) 这种写法就没问题了。在 FP 流行的今天,这个技巧的使用真的是十分常见的,所以请养成想要返回 Object 时,直接先敲 ({}) 的习惯。

三、模板字符串(Template String)

Babel 的文档里面说,“This is similar to string interpolation features in Perl, Python and more.”,然而我并不觉得它跟 string interpolation 有什么本质上的不同。这里介绍一个技巧,如果你想 google 其它语言的这个语法的话,比如 Kotlin,那么你应该用“string interpolation kotlin”作关键字,搜 template 可能不一定是你想要的结果,但 interpolation 一定能搜到。

这个语法本身已经是现在语言的标配了,用标准键盘左上角的 `` 来包裹字符,可以跨行,里面全部的 ${EXPRESSION} 都会变成 EXPRESSION.toString() 插入到字符串中。

但是 JS 的这个语法还有一个神奇的用法,废话不多说直接上代码:

1 function test() { return JSON.stringify(arguments); }
2 test`foo${123}bar${456+789}baz`

你将会看到这样的结果:{"0":["foo","bar","baz"],"1":123,"2":1245} 。第一个参数是正常的字符串部分,按照插入表达式的地方给切开了,剩下的参数则是其它表达式的值。虽然你不太可能会有机会需要用这语法来实现自己的库的功能,但还是有挺大的机会碰到第三方库提供这样的“语法支持”,比如 React 社区一些 CSS 库。当你看到的时候,就不会觉得奇怪,到底人家的库是怎么写的了。

四、模块(Module)

作为一门现代语言,没有模块化的设计是不可能达到工程级的使用的。过去我们在使用 JS 的时候,都是在 HTML 里导入 JavaScript 库比如 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>。但这么做会带来一个问题就是可能会污染全局变量,一个现实的结果就是,假如有两个库都用了同一个全局变量名,那么你就只能很小心的在引用两个库的中间插入一段代码,重命名其中一个全局变量的名称。另外一个问题是,当一个库需要使用另外一个库的时候,必须严格遵守一定的顺序。

模块化之后,带来了两个好处,一个是可以自由的在本模块中自由的命名,另一个是使用 webpack 等工具打包时,可以只打包使用到的代码,从而减小需要发布的 JS 文件大小,节省网络传输时间加快页面加载速度。

遗憾的是,由于 JS 的模块化并没有统一的标准,导致出现了各种不同的加载方式。不过由于 webpack 和 babel 的存在,在写前端代码时,你只要使用 export/import 这一套语法就够了。但如果写 Node 代码,而你又不想使用 Babel 的话,那就得注意一下,由于打包方式不同,一些在导入 default 时会要求 require(XXX).default,比如 redux-thunk。不过这种情况一般也都是前端库的代码,如果纯后端的话应该也不太需要担心。

五、二进制与八进制数字(Binary & Octal Literals)

这个语法也挺简单的,跟16进制类似,0b 或者 0B 是2进制的前缀,用字母 O 替换 X 就是8进制的前缀。但是有一点需要注意的地方,你自己写代码的时候用用可以,但是不要指望 parseInt 可以像对十六进制一样起作用。而像 isNaN、Number 之类的,跟浏览器本身的支持也有关,就算你上了 polyfill 也不一定会支持。当然大家会用这个语法的概率也不大。

六、其他

关于 Symbol(符号)、Set、Map、WeekSet、WeekMap,个人感觉不使用其实影响也不大,而且 polyfill 也不能做到完全支持,做前端就不要考虑了。for ... of 的语法意义也不大,毕竟现在主流已经不推荐写 for 了。iterator 本身没有 Generator 好用,polyfill 支持也有限,所以也没啥意义。剩下的 API 变化也没什么值得说的,前端也只要注意加个 babel-polyfill 之类的就够了。

Proxy、Reflect、Decorator 算是 meta-programming 的内容,需要读我的文章想来水平还不到有机会写神奇功能的水平。如果想实现一些 fancy 的功能,这几个东西还是很有用的。举个例子,比如你想监视(observe)对象成员值的变化,老的做法你就得跑过去,获取这个对象当前所有的属性,然后再都给封装一遍,就算这样如果人家再加了个新成员变量你还是没办法知道。上了 Proxy 你就不用那么麻烦了,所有的操作都会在你的 proxy handler 里面过一遍,实现起来就简单多了。

 

其它的语法,Promise、async/await、Generator 会在异步里面讲,函数的变化、class、Rest/Spread、Destructing 相关性比较高也会放在同一个章节里面讲。