历时将近 6 年的时间来制定的新 ECMAScript 标准 ECMAScript 6(亦称 ECMAScript Harmony,简称 ES6)终于在 2015 年 6 月正式发布。自从上一个标准版本 ES5 在 2009 年发布以后,ES6 就一直以新语法、新特性的优越性吸引着众多 JavaScript 开发者,驱使他们积极尝鲜。
虽然至今各大浏览器厂商所开发的 JavaScript 引擎都还没有完成对 ES2015 中所有特性的完美支持,但这并不能阻挡工程师们对 ES6 的热情,于是乎如、等编译器便出现了。它们能将尚未得到支持的 ES2015 特性转换为 ES5 标准的代码,使其得到浏览器的支持。其中,babel 因其模块化转换器 (Transformer) 的设计特点赢得了绝大部份 JavaScript 开发者的青睐,本文也将以 babel 为基础工具,向大家展示 ES2015 的神奇魅力。
笔者目前所负责的项目中,已经在前端和后端全方位的使用了 ES2015 标准进行 JavaScript 开发,已有将近两年的 ES2015 开发经验。如今 ES2015 以成为 ECMA 国际委员会的首要语言标准,使用 ES2015 标准所进行的工程开发已打好了坚实的基础,而 ES7(ES2016) 的定制也走上了正轨,所以在这个如此恰当的时机,我觉得应该写一篇通俗易懂的 ES2015 教程来引导广大 JavaScript 爱好者和工程师向新时代前进。若您能从本文中有所收获,便是对我最大的鼓励。
我希望你在阅读本文前,已经掌握了 JavaScript 的基本知识,并具有一定的 Web App 开发基础和 Node.js 基本使用经验。
、
- let
和块级作用域
- const
- function
- __proto__
- Promise
- Symbol
(代理)
- Proxy
- async/await
本文的实战部份将以开发一个动态博客系统为背景,向大家展示如何使用 ES2015 进行项目开发。成品代码将在 GitHub 上展示。
说到 ES2015,有了解过的同学一定会马上想到各种新语法,如箭头函数(
)、
- =>
、模板字符串等。是的,ECMA 委员会吸取了许多来自全球众多 JavaScript 开发者的意见和来自其他优秀编程语言的经验,致力于制定出一个更适合现代 JavaScript 开发的标准,以达到 "和谐"(Harmony)。一言蔽之:
- class
ES2015 标准提供了许多新的语法和编程特性以提高 JavaScript 的开发效率和体验
从 ES6 的别名被定为 Harmony 开始,就注定了这个新的语言标准将以一种更优雅的姿态展现出来,以适应日趋复杂的应 用开发需求。
如果您有其他语言(如、)或是某些 JavaScript 的衍生语言(如、)的开发经验,就一定会了解一些很有意思的,如 Ruby 中的
,Scala 和 CoffeeScript 中的箭头函数
- Range -> 1..10
。ECMA 委员会借鉴了许多其他编程语言的标准,给 ECMAScript 家族带来了许多可用性非常高的语法糖,下文将会一一讲解。
- (a, b) => a + b
这些语法糖能让 JavaScript 开发者更舒心地开发 JavaScript 应用,提高我们的工作效率,多一些时间出去浪。
ES2015 除了提供了许多语法糖以外,还由官方解决了多年来困扰众多 JavaScript 开发者的问题:JavaScript 的模块化构建。从许多年前开始,各大公司、团队、大牛都相继给出了他们对于这个问题的不同解决方案,以至于定下了如 CommonJS、AMD、CMD 或是 UMD 等 JavaScript 模块化标准,、、、、等模块加载库都以各自不同的优势占领着一方土地。
然而正正是因为这春秋战国般的现状,广大的前端搬砖工们表示很纳闷。
这™究竟哪种好?哪种适合我?求大神带我飞!
对此,ECMA 委员会终于是坐不住了,站了起来表示不服,并制订了 ES2015 的原生模块加载器标准。
- import fs from 'fs'import readline from 'readline'import path from 'path'let Module = {
- readLineInFile(filename, callback = noop, complete = noop) {
- let rl = readline.createInterface({
- input: fs.createReadStream(path.resolve(__dirname, './big_file.txt'))
- }) rl.on('line', line = >{
- //... do something with the current line
- callback(line)
- }) rl.on('close', complete) return rl
- }
- }
- function noop() {
- return false
- }
- export
- default Module
老实说,这套模块化语法不禁让我们又得要对那个很 silly 的问题进行重新思考了:JavaScript 和 Java 有什么关系?
可惜的是,目前暂时还没有任何浏览器厂商或是 JavaScript 引擎支持这种模块化语法。所以我们需要用 babel 进行转换为 CommonJS、AMD 或是 UMD 等模块化标准的语法。
经过以上的介 (xun) 绍(tao),相信你对 ES2015 也有了一定的了解和期待。接下来我将带大家慢慢看看 ECMA 委员会含辛茹苦制定的新语言特性吧。
、
- let
和块级作用域
- const
在 ES2015 的新语法中,影响速度最为直接,范围最大的,恐怕得数
和
- let
了,它们是继
- const
之后,新的变量定义方法。与
- var
相比,
- let
更容易被理解:
- const
也就是 constant 的缩写,跟 C/C++ 等经典语言一样,用于定义常量,即不可变量。
- const
但由于在 ES6 之前的 ECMAScript 标准中,并没有原生的实现,所以在降级编译中,会马上进行引用检查,然后使用
代替。
- var
- // foo.js
- const foo = 'bar'foo = 'newvalue'$ babel foo.js...SyntaxError: test.js: Line 3 : "foo"is read - only 1 | const foo = 'bar'2 | >3 | foo = 'newvalue'...
在 ES6 诞生之前,我们在给 JavaScript 新手解答困惑时,经常会提到一个观点:
JavaScript 没有块级作用域
在 ES6 诞生之前的时代中,JavaScript 确实是没有块级作用域的。这个问题之所以为人所熟知,是因为它引发了诸如历遍监听事件需要使用闭包解决等问题。
- <button>
- 一
- </button>
- <button>
- 二
- </button>
- <button>
- 三
- </button>
- <button>
- 四
- </button>
- <div id="output">
- </div>
- <script>
- var buttons = document.querySelectorAll('button') var output = document.querySelector('#output') for (var i = 0; i < buttons.length; i++) {
- buttons[i].addEventListener('click',
- function() {
- output.innerText = buttons[i].innerText
- })
- }
- </script>
前端新手非常容易写出类似的代码,因为从直观的角度看这段代码并没有语义上的错误,但是当我们点击任意一个按钮时,就会报出这样的错误信息:
- Uncaught TypeError: Cannot read property 'innerText'of undefined
出现这个错误的原因是因为
不存在,即为
- buttons[i]
。
- undefined
为什么会出现按钮不存在结果呢?通过排查,我们可以发现,每次我们点击按钮时,事件监听回调函数中得到的变量
都会等于
- i
,也就是这里的 4。而
- buttons.length
恰恰不存在,所以导致了错误的发生。
- buttons[4]
再而导致
得到的值都是
- i
的原因就是因为 JavaScript 中没有块级作用域,而使对
- buttons.length
的变量引用 (Reference) 一直保持在上一层作用域(循环语句所在层)上,而当循环结束时
- i
则正好是
- i
。
- buttons.length
而在 ES6 中,我们只需做出一个小小的改动,便可以解决该问题(假设所使用的浏览器已经支持所需要的特性):
- // ...
- for (
- /* var */
- let i = 0; i < buttons.length; i++) {
- // ...
- }
- // ...
- 通過把`
- for`語句中對計數器`i`的定義語句從`
- var`換成`let`,即可。因為`let`語句會使該變量處於一個塊級作用域中,從而讓事件監聽回調函數中的變量引用得到保持。我們不妨看看改進后的代碼經過babel的編譯會變成什麼樣子:
- // ...
- var _loop = function(i) {
- buttons[i].addEventListener('click',
- function() {
- output.innerText = buttons[i].innerText
- })
- }
- for (var i = 0; i < buttons.length; i++) {
- _loop(i)
- }
- // ...
实现方法一目了然,通过传值的方法防止了
的值错误。
- i
继
和
- let
之后,箭头函数就是使用率最高的新特性了。当然了,如果你了解过 Scala 或者曾经如日中天的 JavaScript 衍生语言 CoffeeScript,就会知道箭头函数并非 ES6 独创。
- const
箭头函数,顾名思义便是使用箭头 (
) 进行定义的函数,属于匿名函数(Lambda)一类。当然了,也可以作为定义式函数使用,但我们并不推荐这样做,随后会详细解释。
- =>
箭头函数有好几种使用语法:
- 1.foo = >foo + ' world' // means return `foo + ' world'`
- 2. (foo, bar) = >foo + bar 3.foo = >{
- return foo + ' world'
- }
- 4. (foo, bar) = >{
- return foo + bar
- }
以上都是被支持的箭头函数表达方式,其最大的好处便是简洁明了,省略了
关键字,而使用
- function
代替。
- =>
箭头函数语言简洁的特点使其特别适合用于单行回调函数的定义:
- let names = ['Will', 'Jack', 'Peter', 'Steve', 'John', 'Hugo', 'Mike'] let newSet = names.map((name, index) = >{
- return {
- id: index,
- name: name
- }
- }).filter(man = >man.id % 2 == 0).map(man = >[man.name]).reduce((a, b) = >a.concat(b)) console.log(newSet) //=> [ 'Will', 'Peter', 'John', 'Mike' ]
如果你有 Scala + 的开发经验,就一定会觉得这非常亲切,因为这跟其中的 RDD 操作几乎如出一辙:
的对象,
- { id, name }
则为每个名字在原数组中的位置
- id
为奇数的元素,只保留
- id
为偶数的元素
- id
事实上,箭头函数在 ES2015 标准中,并不只是作为一种新的语法出现。就如同它在 CoffeeScript 中的定义一般,是用于对函数内部的上下文(
)绑定为定义函数所在的作用域的上下文。
- this
- let obj = {
- hello: 'world',
- foo() {
- let bar = () = >{
- return this.hello
- }
- return bar
- }
- }
- window.hello = 'ES6'window.bar = obj.foo() window.bar() //=> 'world'
上面代码中的
等价于 (不代表等于):
- obj.foo
- // ...
- foo() {
- let bar = (function() {
- return this.hello
- }).bind(this) return bar
- }
- // ...
为什么要为箭头函数给予这样的特性呢?我们可以假设出这样的一个应用场景,我们需要创建一个实例,用于对一些数据进行查询和筛选。
- let DataCenter = {
- baseUrl: 'http://example.com/api/data',
- search(query) {
- fetch(`$ {
- this.baseUrl
- }
- /search?query=${query}`)
- .then(res => res.json())
- .then(rows => {
- / / TODO
- })
- }
- }
此时,从服务器获得数据是一个 JSON 编码的数组,其中包含的元素是若干元素的 ID,我们需要另外请求服务器的其他 API 以获得元素本身(当然了,实际上的 API 设计大部份不会这么使用这么蛋疼的设计)。我们就需要在回调函数中再次使用
这个属性,如果要同时兼顾代码的可阅读性和美观性,ES2015 允许我们这样做。
- this.baseUrl
- let DataCenter = {
- baseUrl: 'http://example.com/api/data',
- search(query) {
- return fetch(`$ {
- this.baseUrl
- }
- /search?query=${query}`)
- .then(res => res.json())
- .then(rows => {
- return fetch(`${this.baseUrl}/fetch ? ids = $ {
- rows.join(',')
- }`)
- // 此處的 this 是 DataCenter,而不是 fetch 中的某個實例
- }).then(res = >res.json())
- }
- }
- DataCenter.search('iwillwen').then(rows = >console.log(rows))
因为在单行匿名函数中,如果
指向的是该函数的上下文,就会不符合直观的语义表达。
- this
另外,要注意的是,箭头函数对上下文的绑定是强制性的,无法通过
或
- apply
方法改变其上下文。
- call
- let a = {
- init() {
- this.bar = () = >this.dam
- },
- dam: 'hei',
- foo() {
- return this.dam
- }
- }
- let b = {
- dam: 'ha'
- }
- a.init() console.log(a.foo()) //=> hei
- console.log(a.foo.bind(b).call(a)) //=> ha
- console.log(a.bar.call(b)) //=> hei
另外,因为箭头函数会绑定上下文的特性,故不能随意在顶层作用域使用箭头函数,以防出错:
- // 假設當前運行環境為瀏覽器,故頂層作上下文為 `window`
- let obj = {
- msg: 'pong',
- ping: () = >{
- return this.msg // Warning!
- }
- }
- obj.ping() //=> undefined
- let msg = 'bang!'obj.ping() //=> bang!
为什么上面这段代码会如此让人费解呢?
我们来看看它的等价代码吧。
- let obj = {
- // ...
- ping: (function() {
- return this.msg // Warning!
- }).bind(this)
- }
- // 同樣等價于
- let obj = {
- /* ... */
- }
- obj.ping = (function() {
- return this.msg
- }).bind(this
- /* this -> window */
- )
模板字符串模板出现简直对 Node.js 应用的开发和 Node.js 自身的发展起到了相当大的推动作用!我的意思并不是说这个原生的模板字符串能代替现有的模板引擎,而是说它的出现可以让非常多的字符串使用变得尤为轻松。
模板字符串要求使用 ` 代替原本的单 / 双引号来包裹字符串内容。它有两大特点:
模板字符串之所以称之为 "模板",就是因为它允许我们在字符串中引用外部变量,而不需要像以往需要不断地相加、相加、相加……
- let name = 'Will Wen Gunn'let title = 'Founder'let company = 'LikMoon Creation'let greet = `Hi,
- I 'm ${name}, I am the ${title} at ${company}`
- console.log(greet) //=> Hi, I'm Will Wen Gunn,
- I am the Founder at LikMoon Creation
在 Node.js 中,如果我们没有支持换行的模板字符串,若需要拼接一条 SQL,则很有可能是这样的:
- var sql = "SELECT * FROM Users " + "WHERE FirstName='Mike' " + "LIMIT 5;"
或者是这样的:
- var sql = ["SELECT * FROM Users", "WHERE FirstName='Mike'", "LIMIT 5;"].join(' ')
无论是上面的哪一种,都会让我们感到很不爽。但若使用模板字符串,仿佛打开了新世界的大门~
- let sql = `SELECT * FROM Users WHERE FirstName = 'Mike'LIMIT 5;`
Sweet! 在 Node.js 应用的实际开发中,除了 SQL 的编写,还有如 Lua 等嵌入语言的出现(如 Redis 中的 SCRIPT 命令),或是手工的 XML 拼接。模板字符串的出现使这些需求的解决变得不再纠结了~
看到这个标题的时候,相信有很多同学会感到奇怪,对象字面量还有什么可以扩展的?
确实,对象字面量的语法在 ES2015 之前早已挺完善的了。不过,对于聪明的工程师们来说,细微的改变,也能带来不少的价值。
- function
这个新特性可以算是比较有用但并不是很显眼的一个。
- let obj = {
- // before
- foo: function() {
- return 'foo'
- },
- // after
- bar() {
- return 'bar'
- }
- }
注入
- __proto__
在 ES2015 中,我们可以给一个对象硬生生的赋予其
,这样它就可以成为这个值所属类的一个实例了。
- __proto__
- class Foo {
- constructor() {
- this.pingMsg = 'pong'
- }
- ping() {
- console.log(this.pingMsg)
- }
- }
- let o = {
- __proto__: new Foo()
- }
- o.ping() //=> pong
什么?有什么卵用?
有一个比较特殊的场景会需要用到:我想扩展或者覆盖一个类的方法,并生成一个实例,但觉得另外定义一个类就感觉浪费了。那我可以这样做:
- let o = {
- __proto__: new Foo(),
- init() {
- this.pingMsg = 'alive'
- },
- msg: 'bang',
- yell() {
- console.log(this.msg)
- }
- }
- o.init() o.yell() //=> bang
- o.ping() //=> alive
也是看上去有点鸡肋的新特性,不过在做 JavaScript 模块化工程的时候则有了用武之地。
- // module.js
- export
- default {
- someMethod
- }
- function someMethod() {
- // ...
- }
- // app.js
- import Module from './module'Module.someMethod()
这个特性相当有意思,也是可以用在一些特殊的场景中。
- let arr = [1, 2, 3] let outArr = arr.map(n = >{
- return { [n] : n,
- [`$ {
- n
- } ^ 2`] : Math.pow(n, 2)
- }
- }) console.dir(outArr) //=>
- [{
- '1': 1,
- '1^2': 1
- },
- {
- '2': 2,
- '2^2': 4
- },
- {
- '3': 3,
- '3^2': 9
- }]
在上面的两个
中,我演示了动态计算的对象属性名称的使用,分别为对应的对象定义了当前计数器
- [...]
和
- n
的 2 次方
- n
来了来了来了,相当有用的一个特性。有啥用?多重复值听过没?没听过?来看看吧!
- // Matching with object
- function search(query) {
- // ...
- // let users = [ ... ]
- // let posts = [ ... ]
- // ...
- return {
- users: users,
- posts: posts
- }
- }
- let {
- users,
- posts
- } = search('iwillwen')
- // Matching with array
- let[x, y] = [1, 2]
- // missing one
- [x, , y] = [1, 2, 3]
- function g({
- name: x
- }) {
- console.log(x)
- }
- g({
- name: 5
- })
还有一些可用性不大,但也是有一点用处的:
- // Fail-soft destructuring
- var [a] = [] a === undefined //=> true
- // Fail-soft destructuring with defaults
- var [a = 1] = [] a === 1 //=> true
这个特性有非常高的使用频率,一个简单的语法糖解决了从前需要一两行代码才能实现的功能。
这个特性在类库开发中相当有用,比如实现一些可选参数:
- import fs from 'fs'import readline from 'readline'import path from 'path'
- function readLineInFile(filename, callback = noop, complete = noop) {
- let rl = readline.createInterface({
- input: fs.createReadStream(path.resolve(__dirname, filename))
- }) rl.on('line', line = >{
- //... do something with the current line
- callback(line)
- }) rl.on('close', complete) return rl
- }
- function noop() {
- return false
- }
- readLineInFile('big_file.txt', line = >{
- // ...
- })
我们知道,函数的
和
- call
在使用上的最大差异便是一个在首参数后传入各个参数,一个是在首参数后传入一个包含所有参数的数组。如果我们在实现某些函数或方法时,也希望实现像
- apply
一样的使用方法,在 ES2015 之前,我们可能需要这样做:
- call
- function fetchSomethings() {
- var args = [].slice.apply(arguments)
- // ...
- }
- function doSomeOthers(name) {
- var args = [].slice.apply(arguments, 1)
- // ...
- }而在ES2015中,我們可以很簡單的使用`…`語法糖來實現:
- function fetchSomethings(...args) {
- // ...
- }
- function doSomeOthers(name, ...args) {
- // ...
- }
要注意的是,
后不可再添加
- ...args
虽然从语言角度看,
和
- arguments
是可以同时使用,但有一个特殊情况则不可:
- ...args
在箭头函数中,会跟随上下文绑定到上层,所以在不确定上下文绑定结果的情况下,尽可能不要再箭头函数中再使用
- arguments
,而使用
- arguments
。
- ...args
虽然 ECMA 委员会和各类编译器都无强制性要求用
代替
- ...args
,但从实践经验看来,
- arguments
确实可以在绝大部份场景下可以代替
- ...args
使用,除非你有很特殊的场景需要使用到
- arguments
和
- arguments.callee
。所以我推荐都使用
- arguments.caller
而非
- ...args
。
- arguments
PS:在严格模式(Strict Mode)中,
和
- arguments.callee
是被禁止使用的。
- arguments.caller
在 ES2015 中,
语法还有另外一个功能:无上下文绑定的
- ...
。什么意思?看看代码你就知道了。
- apply
- function sum(...args) {
- return args.map(Number).reduce((a, b) = >a + b)
- }
- console.log(sum(... [1, 2, 3])) //=> 6
有什么卵用?我也不知道 (⊙o⊙)... Sorry...
默认参数值和后续参数需要遵循顺序原则,否则会出错。
- function(...args, last = 1) {
- // This will go wrong
- }
另外,根据函数调用的原则,无论是默认参数值还是后续参数都需要小心使用。
在介绍新的数据结构之前,我们先复习一下在 ES2015 之前,JavaScript 中有哪些基本的数据结构。
其中又分为值类型和引用类型,Array 其实是 Object 的一种子类。
我们再来复习下高中数学吧,集不能包含相同的元素,我们可以根据元素画出多个集的韦恩图…………
好了跑题了。是的,在 ES2015 中,ECMA 委员会为 ECMAScript 增添了集 (Set) 和 "弱" 集(WeakSet)。它们都具有元素唯一性,若添加了已存在的元素,会被自动忽略。
- let s = new Set() s.add('hello').add('world').add('hello') console.log(s.size) //=> 2
- console.log(s.has('hello')) //=> true
在实际开发中,我们有很多需要用到集的场景,如搜索、索引建立等。
咦?怎么还有一个 WeakSet?这是干什么的?我曾经写过一篇关于的文章,而其中大部份都是在语言上动手脚,而 WeakSet 则是在数据上做文章。
WeakSet 在 JavaScript 底层作出调整(在非降级兼容的情况下),检查元素的变量引用情况。如果元素的引用已被全部解除,则该元素就会被删除,以节省内存空间。这意味着无法直接加入数字或者字符串。另外 WeakSet 对元素有严格要求,必须是 Object,当然了,你也可以用
等形式处理元素。
- new String('...')
- let weaks = new WeakSet() weaks.add("hello") //=> Error
- weaks.add(3.1415) //=> Error
- let foo = new String("bar") let pi = new Number(3.1415) weaks.add(foo) weaks.add(pi) weaks.has(foo) //=> true
- foo = null weaks.has(foo) //=> false
从数据结构的角度来说,映射(Map)跟原本的 Object 非常相似,都是 Key/Value 的键值对结构。但是 Object 有一个让人非常不爽的限制:key 必须是字符串或数字。在一般情况下,我们并不会遇上这一限制,但若我们需要建立一个对象映射表时,这一限制显得尤为棘手。
而 Map 则解决了这一问题,可以使用任何对象作为其 key,这可以实现从前不能实现或难以实现的功能,如在项目逻辑层实现数据索引等。
- let map = new Map() let object = {
- id: 1
- }
- map.set(object, 'hello') map.set('hello', 'world') map.has(object) //=> true
- map.get(object) //=> hello
而 WeakMap 和 WeakSet 很类似,只不过 WeakMap 的键和值都会检查变量引用,只要其一的引用全被解除,该键值对就会被删除。
- let weakm = new WeakMap() let keyObject = {
- id: 1
- }
- let valObject = {
- score: 100
- }
- weakm.set(keyObject, valObject) weakm.get(keyObject) //=> { score: 100 }
- keyObject = null weakm.has(keyObject) //=> false
类,作为自 JavaScript 诞生以来最大的痛点之一,终于在 ES2015 中得到了官方的妥协,"实现" 了 ECMAScript 中的标准类机制。为什么是带有双引号的呢?因为我们不难发现这样一个现象:
- $ node > class Foo {} [Function: Foo]
回想一下在 ES2015 以前的时代中,我们是怎么在 JavaScript 中实现类的?
- function Foo() {}
- var foo = new Foo()
是的,ES6 中的类只是一种语法糖,用于定义原型 (Prototype) 的。当然,饿死的厨师三百斤,有总比没有强,我们还是很欣然地接受了这一设定。
与大多数人所期待的一样,ES2015 所带来的类语法确实与很多 C 语言家族的语法相似。
- class Person {
- constructor(name, gender, age) {
- this.name = name this.gender = gender this.age = age
- }
- isAdult() {
- return this.age >= 18
- }
- }
- let me = new Person('iwillwen', 'man', 19) console.log(me.isAdult()) //=> true
与 JavaScript 中的对象字面量不一样的是,类的属性后不能加逗号,而对象字面量则必须要加逗号。
然而,让人很不爽的是,ES2015 中对类的定义依然不支持默认属性的语法:
- // 理想型
- class Person {
- name: String gender = 'man'
- // ...
- }
而在 TypeScript 中则有良好的实现。
来源: http://lib.csdn.net/article/reactnative/43601