ES6核心内容精讲

前言

一、新的变量声明方式let和const

是什么:

新的变量声明方式,提供变量的块级作用域,同时通过一些限制来更防止我们犯错误。也就是说是更好的声明变量的方式

怎么用

  • var 的问题

    • 可以重复声明,没有报错和警告
    • 无法限制修改
    • 没有块级作用域, { }
  • let 和 const

    • 不能重复声明
    • 都是块级作用域, { } 块内声明的,块外无效
    • let 是变量,可以修改
    • const 是常量,不能修改

1)let/const与var的区别是提供了块级作用域以及变量不会变量提升

    if(true){
      let temp=123
    }
    console.log(temp)

temp在if这个代码块内声明,只在当前代码块内有效。

2)在同一个作用域内let/const禁止重复声明相同的变量

var a = 1
let a = 2 // SyntaxError

3)let声明的变量可重新赋值,const声明的变量不能重新赋值,即常量。

4)暂时性死区:在当前作用域,使用的变量已经存在,但是在代码执行到变量声明前禁止访问。

var tmp = 123

if (true) {
  tmp = 'abc' // ReferenceError
  let tmp
}

结果:

temp=true
let temp

常见使用场景

1)因为能创建块级作用域,所以常见于iffor

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i])
}

2)const在实践中常用来声明一个对象,之后可以再对这个对象的属性进行修改

const foo = {
    name: 'bar'
}

foo.name = 'baz'

console.log(foo)

你也可以声明一个数组,对数组里面的值改变

    const foo=[234,234,456,567]
    foo[2]=1111111
    console.log(foo) // [234, 234, 1111111, 567]

二、解构

是什么:

按照阮一峰大神的说法:ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。也就是说通过模式匹配来进行变量赋值。

怎么用:

1)数组基本用法

let [a, b, c] = [1, 2, 3]

a //1
b //2
c //3

2)对象基本用法

let { foo, bar } = { foo: "aaa", bar: "bbb" }
foo // "aaa"
bar // "bbb"

3)函数参数的解构赋值

如在vuex中action不使用解构如下:

actions: {
    increment (context) {
      context.commit('increment')
    }
}

使用解构

actions: {
    increment ({ commit }) {
      commit('increment')
    }
}

4)支持不完全解构

let [foo, bar] = [1, 2, 3]

5)如果解构不成功,变量的值就等于undefined,同时解构赋值允许指定默认值,默认值生效的条件是对象的属性值严格等于undefined。

嵌套赋值

    let{man:{name}}={man:{name:'zjx',age:23}}
    console.log(name) //zjx
    console.log(age)  //a.html:19 Uncaught ReferenceError: age is not defined
    console.log(man)  //a.html:20 Uncaught ReferenceError: man is not defined

注意:此时man是模式,不能作为变量,而age没有在模式内也不会赋值

解构赋值默认值

    var {name="zjx"}={name:'liurong',age:23}
    console.log(name) //liurong

    //解构赋值 可以设置默认值 当值为undefined的时候默认值才会生效
    var {name="zjx"}={name:undefined,age:23}
    console.log(name)  //zjx
    var {name="zjx"}={name:null,age:23}
    console.log(name) //null

    var [name="zjx"]=[undefined,23]
    console.log(name)  //zjx
    var [name="zjx"]=[null,23]
    console.log(name) //null

常见使用场景

1)交换变量的值

[x, y] = [y, x]

2)提取JSON数据

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
}

let { id, status, data: number } = jsonData

console.log(id, status, number) // 42, "OK", [867, 5309]

3)函数参数的默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
}

4)指定加载模块的什么功能

import { mapActions } from 'vuex'


三、箭头函数

干嘛的:

箭头函数可以用来替换函数表达式,不用写function,更加简化。也就是说是函数表达式的简化方式

箭头函数,就是函数的简写

  • 如果只有一个参数,() 可以省
  • 如果只有一个return{}可以省

怎么用:

1)注意箭头函数中的this指向外层的this

2)无法用call/apply/bind来改变this指向。

3)在ES6中,会默认采用严格模式,因此默认情况下this不是指向window对象,而是undefined。

<script type="text/javascript">
    setTimeout(() => console.log(this), 1000) // undefined,不是window
</script>

4)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。

使用场景:

关键是你需要this指向什么

 箭头函数中没有this,this是继承于父级作用域的

例子:

原因:js中的this是运行的时候才绑定的,map里面是个独立函数,当一个函数独立运行的时候(不是作为对象的方法调用或call,apply,bind调用),this指向window,严格模式指向undefined

 

解决方法1:通过定义一个变量保存this

方法2:使用箭头函数

原因:箭头函数没有自己的this值,他的this值是继承他的父作用域。与我们之前接触的this是运行时绑定的不同,箭头函数的this是作用在词法作用域的。不会随着调用方法的改变而改变

箭头函数不适用场景

改正

改正

 

原因:箭头函数是没有argument对象的,要想使用argument对象,就不能使用箭头函数

四、默认参数和rest参数(用于函数)

是什么:

默认参数就是设置参数默认值,rest参数(翻译为不具名参数,也叫做剩余参数)是将传入的未具名的参数作为一个数组集合

怎么用:

如下,默认参数给参数赋一个默认值,rest参数使用三个点(...)加数组集合名

function foo(arg1 = 1, ...restArg){
    console.log(arg1, restArg)
}

foo(undefined, 2, 3, 4) // 1, [2, 3, 4]

foo(2, 3, 4) // 2, [3, 4]
例子:

使用函数默认值

函数默认值实际上是通过判断参数是否为undefined,是unefined时使用默认值。

根据type of a === undefined

传入null 则 a=null 而不是默认值

 

五、扩展运算符

是什么:

同rest参数一样,也是三个点。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。因此常用于函数调用。

常见使用场景:

1)合并数组

let newArr = [...arr1, ...arr2, ...arr3]

2)与解构赋值结合

// ES5
a = list[0], rest = list.slice(1)

// ES6
[a, ...rest] = list

3)将字符串转为数组

[...'test'] // [ "t", "e", "s", "t"]

对象扩展

语法变化

1)属性简写,当对象的一个属性名称与本地变量名相同的时候,可以省略冒号和值

var foo = 'bar'
var baz = {foo}
baz // {foo: "bar"}

2)属性名表达式,可以在以对象字面量方式定义对象是使用表达式作为属性名

// ES5只能这样
obj['a' + 'bc'] = 123 

// ES6还能这样
let obj = {
  ['a' + 'bc']: 123
}

3)方法简写,省去:和function

const foo = {
  bar () {
    console.log('1')
  }
}

Object.is()

更好的判断方法,与===的不同有两点:一是+0不等于-0,二是NaN等于自身。

NaN === NaN // false
Object.is(NaN, NaN) // true

Object.assign()

1)Object.assign方法用于对象的合并,用法与jQuery和underscore的extend方法类似,而且同样会改变target。

Object.assign(target, source1, source2)

2)只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

3)Object.assign方法实行的是浅拷贝,而不是深拷贝。

Object.setPrototypeOf

用来设置一个对象的prototype对象,返回参数对象本身

Object.setPrototypeOf(object, prototype)

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。因此,可以使用这个方法判断,一个类是否继承了另一个类。参见下面类与继承章节

遍历

对象的每个属性都有一个描述对象(Descriptor),Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。这个描述对象有value、writable、enumerable、configurable四大属性。

ES5下面三个操作会忽略enumerable为false的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性
  • Object.keys():返回对象自身的所有可枚举的属性的键名
  • JSON.stringify():只串行化对象自身的可枚举的属性

ES6新增的操作Object.assign(),也会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

ES6一共有5种方法可以遍历对象的属性。

(1)for...in

for...in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。

(2)Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。

(3)Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。

(4)Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。

(5)Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举。

以上的5种方法遍历对象的属性,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有属性名为数值的属性,按照数字排序。
  • 其次遍历所有属性名为字符串的属性,按照生成时间排序。
  • 最后遍历所有属性名为Symbol值的属性,按照生成时间排序。

大多数时候,我们只关心对象自身的可枚举属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

 

六、字符串扩展

以前判断一个字符串是否包含某个字符串只能通过indexOf的值是否大于-1来判断,现在新增了三种方法:

includes():表示是否包含该字符串。

startsWith():表示该字符串是否在头部。  区分大小写

endsWith():表示该字符串是否在尾部。   区分大小写

'hello world'.includes('hello') // true
以上三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
"上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束 

模板字符串

干嘛的:

和handlebars那些模板引擎功能类似,有模板字符串可以不用拼接字符串了

怎么用:

用反引号``将整个字符串包裹起来,${}表示一个变量或者一个表达式,可以嵌套

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`

const data = [
  { first: 'Jane', last: 'Bond' },
  { first: 'Lars', last: 'Croft' },
]

console.log(tmpl(data))

标签模板

函数名后面紧接一个模板字符串。该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。当字符串模板有变量时,函数的第一个参数为被变量分开的字符串组成的数组,后面的参数依次为变量,这些变量的参数序列可以使用rest参数。

var a = 5
var b = 10

tag`Hello ${ a + b } world ${ a * b }`
// 等同于
tag(['Hello ', ' world ', ''], 15, 50)

七、数组扩展

Array.of()

Array.of方法用于将一组值,转换为数组。可以替代Array,且其行为非常统一,不像Array只有一个正整数参数n时,会生成n个空位构成的数组

Array.of(1) // [1]
Array.of(1, 2, 3) // [1, 2, 3]
Array(1) // [undefined * 1],其实不是undefined,是空位,如下可证明两者并不一样
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
Array(1, 2, 3) // [1, 2, 3]

Array(x)只传一个数的时候,会产生长度为x的数组,其他为empty的数组,二Array.of()则能正确生成[x]
    var data1=Array(4)
    console.log(data1)
    var data2=Array.of(4)
    console.log(data2)

Array.from()

Array.from方法用于将两类对象转为真正的数组:类数组对象(array-like object)和可遍历(iterable)对象(包括ES6新增的数据结构Set和Map)。Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from({'0': 'a', length: 1}) // ['a']

Array.from('hello') // ['h', 'e', 'l', 'l', 'o'],因为字符串有Iterator接口,可遍历

Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9]

常见使用场景:

1)转换NodeList集合。常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象。但是后者使用rest参数更简便

// NodeList对象
let elementDivList = document.querySelectorAll('div')
Array.from(elementDivList).forEach(function (div) {
  console.log(div)
})

2)数组去重。与下面要讲到的Set配合即可很简单地实现数值去重。

var arr = [1, 3, 5, 5, 8, 3, 2]
var uniqueArr = Array.from(new Set(arr))
console.log(uniqueArr) // [1, 3, 5, 8, 2]
    var arr=[2,343,6,2134,4562,34,2,343,5,6]
    var res=new Set(arr)
    console.log(res)

打印的结果为

通过Array.from转为真正的数组

    var arr=[2,343,6,2134,4562,34,2,343,5,6]
    var res=new Set(arr)
    console.log(res)
    var result=Array.from(res)
    console.log(result)

Array.from传入第二个参数,相当于map方法

数组实例的copyWithin方法

Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

  • target(必需):从该位置开始替换数据。
  • start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
    [1, 2, 3, 4, 5].copyWithin(0,2) // [3, 4, 5, 4, 5]
    /* 从索引2开始读取数据,到数组尾部停止,即读到(3, 4, 5),然后从索引0开始替换数据 */

数组实例的find和findIndex方法

找到第一个符合条件的item(项)或index(索引),前者相当于underscore中的first方法,后者则和underscore中的同名方法一致。另外,这两个方法都可以借助Object.is发现NaN,弥补了数组的IndexOf方法的不足。

[1, 2, 3, 4].find(x => x > 2) // 3
[1, 2, 3, 4].findIndex(x => x > 2) // 2
[NaN].findIndex(x => Object.is(NaN, x)) // 0

数组实例的fill方法

Array.prototype.fill(fillItem, start = 0, end = this.length)

[1, 3, 6, 11, 4].fill(10,2) // [1, 3, 10, 10, 10]

数组实例的includes方法

与字符串的includes方法类似。该方法属于ES7,但Babel转码器已经支持。

返回true false

[1, 2, 3].includes(3, 3) // false
[1, 2, 3].includes(3, -1) // true
[NaN].includes(NaN) // true

 数组实例的some方法

数组中有一个满足测试函数,就返回true,并停止后面运行

数组实例的every方法

 数组中全部满足测试函数,就返回true,若有个一不满足,则返回 false,停止后面运行

八、Set和Map

ES6中增加了两个新的数据结构:Set和Map。Set是不包含重复值的列表,而Map则是键与相对应的值的集合。

Set

是什么:

Set是不包含重复值的有序列表。

怎么用:

1)Set构造函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。

const set = new Set([1, 2, 3, 4, 4])
console.log(set)

2)四个操作方法(add()、delete()、has()、clear())和一个属性(size),使用方法根据名字和下面例子就知道了

const s = new Set()
s.add(1).add(2).add(2) // 注意2被add了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2)
s.has(2) // false

s.add(3)
s.size // 2

s.clear()
s.size // 0

3)三个遍历器生成函数(keys()、values()、entries())和一个遍历方法(forEach())

keys方法、values方法、entries方法返回的都是遍历器对象(详见Iterator)。都这可以使用遍历器对象的方法for...of进行遍历。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。forEach方法与ES5数组的forEach类似。

let set = new Set(['red', 'green', 'blue'])

for (let item of set.keys()) {
  console.log(item)
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item)
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item)
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

set.forEach((value, key) => console.log(value, key))

4)Set转化为数组,有两种方法:...扩展运算符和Array.from()

这两者可互换,因此前面提到的使用Array.from()来数组去重也可以这样做:[...new Set(arr)]

// 方法一
let set = new Set([1, 2, 3])
set = new Set([...set].map(val => val * 2))
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3])
set = new Set(Array.from(set, val => val * 2))
// set的值是2, 4, 6

Map

是什么:

一种由键值对集合构成的数据结构,类似于对象,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

//比如对象必须使用字符串作为键key,
//使用map.set方法,可以用对象作为键
//使用map.get方法,来获取到使用这个对象作为键的值

 

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

//比如对象必须使用字符串作为键key,
//使用map.set方法,可以用对象作为键
//使用map.get方法,来获取到使用这个对象作为键的值

console.log(m)
console.log(Array.from(m))

怎么用:

1)Set构造函数可以接收任何一个具有Iterator接口的数据结构作为参数

const set = new Set([
  ['foo', 1],
  ['bar', 2]
])
const m1 = new Map(set)
m1.get('foo') // 1

2)5个操作方法(set(key, value)、get(key)、has(key)、delete(key)、clear())和一个属性(size)

3)遍历生成函数和遍历方法和Set类似,Map结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

map[Symbol.iterator] === map.entries // true

4)Map转为数组,使用...扩展运算符

WeakSet和WeakMap

WeakSet、WeakMap分别和Set、Map类似,不过存储的是对象的弱引用方式,这样在内存管理上更加容易优化。

九、Iterator和for...of

是什么:

Iterator(迭代器)是专门用来控制如何遍历的对象,具有特殊的接口。

Iterator接口是一种数据遍历的协议,只要调用迭代器对象对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息,这个包含done和value两个属性。

迭代器对象创建后,可以反复调用 next()使用。

怎么用:

Iterator对象带有next方法,每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this
    let index = 0
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          }
        } else {
          return { value: undefined, done: true }
        }
      }
    }
  }
}

for(let item of obj){
    console.log(item)
}
// hello
// world

如上,for-of循环首先调用obj对象的Symbol.iterator方法,紧接着返回一个新的迭代器对象。迭代器对象可以是任意具有.next()方法的对象,for-of循环将重复调用这个方法,每次循环调用一次。return的对象中value表示当前的值,done表示是否完成迭代。

Iterator的作用有三个:

  1. 为各种数据结构,提供一个统一的、简便的访问接口;

  2. 使得数据结构的成员能够按某种次序排列;

  3. ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

for...of循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象(比如arguments对象、DOM NodeList对象)、后文的Generator对象,以及字符串。

for of新的遍历方式 可用于数组、对象、set、map格式数据、元素集合nodelist、arguments等

原有遍历方法劣势

for循环  人类可读性不高

forEach、map等   不能使用 break、continue 和 return 语句中止遍历

for in 遍历数组时的缺陷

var array=['广东','江西','湖南']
array.provience='广西' //可枚举属性  遍历到名为provience索引
for( var index in array){
  console.log(array[index])
}

// 在这段代码中,赋给 index 的值不是实际的数字,而是字符串“0”、“1”、“2”,此时很可能在无意之间进行字符串算数计算,例如:“2” + 1 == “21”,这给编码过程带来极大的不便。
// 作用于数组的 for-in 循环体除了遍历数组元素外,还会遍历自定义属性。举个例子,如果你的数组中有一个可枚举属性 myArray.name,循环将额外执行一次,遍历到名为“name”的索引。就连数组原型链上的属性都能被访问到。
// 最让人震惊的是,在某些情况下,这段代码可能按照随机顺序遍历数组元素。
// 简而言之,for-in 是为普通对象设计的,你可以遍历得到字符串类型的键,因此不适用于数组遍历。

for of的优势

  • 这是最简洁、最直接的遍历数组元素的语法
  •  Map 和 Set 、arguments对象等类数组对象遍历
  • 这个方法避开了 for-in 循环的所有缺陷
  • 与 forEach() 不同的是,它可以正确响应 break、continue 和 return 语句
  • for-of 循环不支持普通对象
  • 遍历的只是数组内的元素,而不包括数组的原型属性method和索引name
var array=['广东','江西','湖南']
array.provience='广西' 
//for in 遍历的时候 参数是key(键),for of遍历的时候是value(值),不遍历索引,可以调用entries() for( var value of array){ console.log(value) //'广东'、'江西'、'湖南' }

十、Symbol

是什么

ES6引入了一种第六种基本类型的数据:Symbol。Symbol是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。

怎么用

调用Symbol()创建一个新的symbol,它的值与其它任何值皆不相等。

var sym = new Symbol() // TypeError,阻止创建一个显式的Symbol包装器对象而不是一个Symbol值
var s1 = Symbol('foo')
var s2 = Symbol('foo')
s1 === s2 // false

常用使用场景:

由于每一个Symbol值都是不相等的,因此常作为对象的属性名来防止某一个键被不小心改写或覆盖,这个以symbol为键的属性可以保证不与任何其它属性产生冲突。

作为对象属性名时的遍历:参见对象的遍历那节

内置的Symbol值:

除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。其中一个很重要的就是Iterator中提到的Symbol.iterator

例子:

十一、Reflect(反射)

是什么

Reflect是一个内置的对象,它提供可拦截JavaScript操作的方法。

为什么要增加Reflect对象

参考链接

1)更有用的返回值

比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

2)函数操作。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为

3)更加可靠的函数调用方式

在ES5中,当我们想传一个参数数组args来调用函数f,并且将this绑定为this,可以这样写:

f.apply(obj, args)

但是,f可能是一个故意或者不小心定义了它自己的apply方法的对象。当你想确保你调用的是内置的apply方法时,一种典型的方法是这样写的:

Function.prototype.apply.call(f, obj, args)

但是这种方法不仅冗长而且难以理解。通过使用Reflect,你可以以一种更简单、容易的方式来可靠地进行函数调用

Reflect.apply(f, obj, args)

4)可变参数的构造函数

假设你想调用一个参数是可变的构造函数。在ES6中,由于新的扩展运算符,你可能可以这样写:

var obj = new F(...args)

在ES5中,这更加难写,因为只有通过F.apply或者F.call传递可变参数来调用函数,但是没有F.contruct来传递可变参数实例化一个构造函数。通过Reflect,在ES5中可以这样写(内容翻译自参考链接,链接的项目是ES6 Reflect和Proxy的一个ES5 shim,所以会这么说):

var obj = Reflect.construct(F, args)

5)为Proxy(代理,见下一章)的traps提供默认行为

当使用Proxy对象去包裹存在的对象时,拦截一个操作是很常见的。执行一些行为,然后去“做默认的事情”,这是对包裹的对象进行拦截操作的典型形式。例如,我只是想在获取对象obj的属性时log出所有的属性:

var loggedObj = new Proxy(obj, {
  get: function(target, name) {
    console.log("get", target, name);
    // now do the default thing
  }
});

Reflect和Proxy的API被设计为互相联系、协同的,因此每个Proxy trap都有一个对应的Reflect去“做默认的事情”。因此当你发现你想在Proxy的handler中“做默认的事情”是,正确的事情永远都是去调用Reflect对象对应的方法:

var loggedObj = new Proxy(obj, {
  get: function(target, name) {
    console.log("get", target, name);
    return Reflect.get(target, name);
  }
});

Reflect方法的返回类型已经被确保了能和Proxy traps的返回类型兼容。

6)控制访问或者读取时的this

var name = ... // get property name as a string
Reflect.get(obj, name, wrapper) // if obj[name] is an accessor, it gets run with `this === wrapper`
Reflect.set(obj, name, value, wrapper)

静态方法

Reflect对象一共有14个静态方法(其中Reflect.enumerate被废弃)

与大多数全局对象不同,Reflect没有构造函数。不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。

Reflect对象提供以下静态函数,它们与代理处理程序方法(Proxy的handler)有相同的名称。这些方法中的一些与Object上的对应方法基本相同,有些遍历操作稍有不同,见对象扩展遍历那节。

Reflect.apply()

对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply()功能类似。

Reflect.construct()

对构造函数进行new操作,相当于执行new target(...args)。

Reflect.defineProperty()

和Object.defineProperty()类似。

Reflect.deleteProperty()

删除对象的某个属性,相当于执行delete target[name]。

Reflect.enumerate()

该方法会返回一个包含有目标对象身上所有可枚举的自身字符串属性以及继承字符串属性的迭代器,for...in 操作遍历到的正是这些属性。

Reflect.get()

获取对象身上某个属性的值,类似于target[name]。

Reflect.getOwnPropertyDescriptor()

类似于Object.getOwnPropertyDescriptor()。

Reflect.getPrototypeOf()

类似于Object.getPrototypeOf()。

Reflect.has()

判断一个对象是否存在某个属性,和in运算符的功能完全相同。

Reflect.isExtensible()

类似于Object.isExtensible().

Reflect.ownKeys()

返回一个包含所有自身属性(不包含继承属性)的数组。

Reflect.preventExtensions()

类似于Object.preventExtensions()。

Reflect.set()

设置对象身上某个属性的值,类似于target[name] = val。

Reflect.setPrototypeOf()

类似于Object.setPrototypeOf()。

十二、Proxy(代理)

是什么

Proxy对象用于定义基本操作的自定义行为 (例如属性查找,赋值,枚举,函数调用等)。

一些术语:

  • handler:包含traps的对象。
  • traps:提供访问属性的方法,与操作系统中的traps定义相似。
  • target:被代理虚拟化的对象,这个对象常常用作代理的存储后端。

用法

ES6原生提供Proxy构造函数,用来生成Proxy实例。

var proxy = new Proxy(target, handler);

Proxy对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要代理的目标对象,handler参数也是一个对象,用来定制代理行为。

下面代码对一个空对象进行了代理,重定义了属性的读取(get)和设置(set)行为。

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

handler对象的方法

handler是一个包含了Proxy的traps的占位符对象。

所有的trap都是可选的,如果某个trap没有定义,将会对target进行默认操作。这些trap和Reflect的静态方法是对应的,可以使用Reflect对应的静态方法提供默认行为。上面的例子中,handler定义了get和set两个trap,每个trap都是一个方法,接收一些参数。返回了对应的Reflect方法来执行默认方法。

handler的每个方法可以理解为对相应的某个方法进行代理拦截。

handler.getPrototypeOf(target):Object.getPrototypeOf的一个trap

handler.setPrototypeOf(target, proto):Object.setPrototypeOf的一个trap

handler.isExtensible(target):Object.isExtensible的一个trap

handler.preventExtensions(target):Object.preventExtensions的一个trap

handler.getOwnPropertyDescriptor(target, propKey):Object.getOwnPropertyDescriptor的一个trap

handler.defineProperty(target, propKey, propDesc):Object.defineProperty的一个trap

handler.has(target, propKey):in操作的一个trap

handler.get(target, propKey, receiver):获取属性值的一个trap

handler.set(target, propKey, value, receiver):设置属性值的一个trap

handler.deleteProperty(target, propKey):delete操作的一个trap

handler.ownKeys(target):Object.getOwnPropertyNames和Object.getOwnPropertySymbols的一个trap

handler.apply(target, object, args):函数调用的一个trap

handler.construct(target, args):new操作的一个trap

Proxy.revocable()

Proxy.revocable方法返回一个可取消的Proxy实例。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

使用场景

上面说的那些可能都比较虚,去看一下w3cplus上翻译的实例解析ES6 Proxy使用场景,可能就会更清楚地明白该怎么用。

如实例解析ES6 Proxy使用场景中所说,Proxy其功能非常类似于设计模式中的代理模式,该模式常用于三个方面:

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

有以下5个常见使用场景:

  1. 抽离校验模块

  2. 私有属性

  3. 访问日志

  4. 预警和拦截

  5. 过滤操作

 

十三、类与继承

类:

将原先JavaScript中传统的通过构造函数生成新对象的方式变为类的方式,contructor内是构造函数执行的代码,外面的方法为原型上的方法

// ES5
function Point(x, y) {
  this.x = x
  this.y = y
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')'
}

var p = new Point(1, 2)

//定义类
class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  // 静态方法,static关键字,就表示该方法不会被实例继承(但是会被子类继承),而是直接通过类来调用
  static classMethod() {
    return 'hello'
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')'
  }
}

继承:

通过extends关键字来实现。super关键字则是用来调用父类

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。理解了这句话,下面1,2两点也就顺其自然了:

1)子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

2)在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)      
        this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString() // 调用父类的toString()
  }
}

Object.getPrototypeOf(ColorPoint) === Point // true

3)mixin: 继承多个类

function mix(...mixins) {
  class Mix {}

  for (let mixin of mixins) {
    copyProperties(Mix, mixin);
    copyProperties(Mix.prototype, mixin.prototype);
  }

  return Mix
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== "constructor"
      && key !== "prototype"
      && key !== "name"
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc)
    }
  }
}

class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}

4)new.target属性:通过检查new.target对象是否是undefined,可以判断函数是否通过new进行调用。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name
  } else {
    throw new Error('必须使用new生成实例')
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name
  } else {
    throw new Error('必须使用new生成实例')
  }
}

var person = new Person('张三') // 正确
var notAPerson = Person.call(person, '张三')  // 报错

十四、Decorator(装饰器)

是什么

Decorator是用来修改类(包括类和类的属性)的一个函数。

这是ES的一个提案,其实是ES7的特性,目前Babel转码器已经支持。

怎么用

1)修饰类:在类之前使用@加函数名,装饰器函数的第一个参数,就是所要修饰的目标类

function testable(target) {
  target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true

装饰器函数也可以是一个工厂方法

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

2)修饰类的属性:修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。装饰器在作用于属性的时候,实际上是通过Object.defineProperty来进行扩展和封装的。

下面是一个例子,修改属性描述对象的enumerable属性,使得该属性不可遍历。

class Person {
  @nonenumerable
  get kidCount() { return this.children.length; }
}

function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

实践

core-decorators.js这个第三方模块提供了几个常见的修饰器。

在修饰器的基础上,可以实现Mixin模式等。

十五、Module(模块)

在ES6之前,前端和nodejs实践中已经有一些模块加载方案,如CommonJS、AMD、CMD等。ES6在语言标准的层面上,实现了模块功能。

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

export

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。必须使用export关键字输出该变量。有以下两种不同的导出方式:

命名导出

命名导出规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

export { myFunction }; // 导出一个函数声明
export const foo = Math.sqrt(2); // 导出一个常量

默认导出 (每个脚本只能有一个),使用export default命令:

export default myFunctionOrClass

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字

对于只导出一部分值来说,命名导出的方式很有用。在导入时候,可以使用相同的名称来引用对应导出的值。

关于默认导出方式,每个模块只有一个默认导出。一个默认导出可以是一个函数,一个类,一个对象等。当最简单导入的时候,这个值是将被认为是”入口”导出值。

import

使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块。

import { foo, bar } from 'my_module' // 指定加载某个输出值

import 'lodash'; // 仅执行

import { lastName as surname } from './profile'; // 为输入的模块重命名

import * as circle from './circle'; // 整体加载

/*export和import复合写法*/
export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

ES6模块与CommonJS模块的差异

它们有两个重大差异。

  • CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用。
  • CommonJS模块是运行时加载,ES6模块是编译时输出接口。

CommonJS是运行时加载,ES6是编译时加载,使得静态分析成为可能

注意事项

  1. ES6的模块自动采用严格模式。因此ES6模块中,顶层的this指向undefined。

  2. export一般放在两头即开始或者结尾这样更能清晰地明白暴露了什么变量

  3. 注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。因为不是运行时加载,不支持条件加载、按需加载等

十六、Promise

是什么

Promise是异步编程的一种解决方案。Promise对象表示了异步操作的最终状态(完成或失败)和返回的结果。

其实我们在jQuery的ajax中已经见识了部分Promise的实现,通过Promise,我们能够将回调转换为链式调用,也起到解耦的作用。

怎么用

Promise接口的基本思想是让异步操作返回一个Promise对象

三种状态和两种变化途径

Promise对象只有三种状态。

  • 异步操作“未完成”(pending)
  • 异步操作“已完成”(resolved,又称fulfilled)
  • 异步操作“失败”(rejected)

这三种的状态的变化途径只有两种。

  • 异步操作从“未完成”到“已完成”
  • 异步操作从“未完成”到“失败”。

这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise对象的最终结果只有两种。

异步操作成功,Promise对象传回一个值,状态变为resolved。

异步操作失败,Promise对象抛出一个错误,状态变为rejected。

生成Promise对象

通过new Promise来生成Promise对象:

var promise = new Promise(function(resolve, reject) {
  // 异步操作的代码

  if (/* 异步操作成功 */){
    resolve(value)
  } else {
    reject(error)
  }
})

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。

resolve会将Promise对象的状态从pending变为resolved,reject则是将Promise对象的状态从pending变为rejected。

Promise构造函数接受一个函数后会立即执行这个函数

var promise = new Promise(function () {
    console.log('Hello World')
})
// Hello World

then和catch回调

Promise对象生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。第二个函数是可选的。分别称之为成功回调和失败回调。成功回调接收异步操作成功的结果为参数,失败回调接收异步操作失败报出的错误作为参数。

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve('成功')
    }, 3000)
})

promise.then(function (data){
    console.log(data)
})
// 3s后打印'成功'

catch方法是then(null, rejection)的别名,用于指定发生错误时的回调函数。

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        reject('失败')
    }, 3000)
})

promise.catch(function (data){
    console.log(data)
})
// 3s后打印'失败'

Promise.all()

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

var p = Promise.all([p1, p2, p3])

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。)

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成resolved,p的状态才会变成resolved,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被Rejected,p的状态就变成Rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race()

与Promise.all()类似,不过是只要有一个Promise实例先改变了状态,p的状态就是它的状态,传递给回调函数的结果也是它的结果。所以很形象地叫做赛跑。

Promise.resolve()和Promise.reject()

有时需要将现有对象转为Promise对象,可以使用这两个方法。

 

十七、Generator(生成器)

是什么

生成器本质上是一种特殊的迭代器(参见本文章系列二之Iterator)。ES6里的迭代器并不是一种新的语法或者是新的内置对象(构造函数),而是一种协议 (protocol)。所有遵循了这个协议的对象都可以称之为迭代器对象。生成器对象由生成器函数返回并且遵守了迭代器协议。具体参见MDN。

怎么用

执行过程

生成器函数的语法为function*,在其函数体内部可以使用yield和yield*关键字。

function* gen(x){
  console.log(1)
  var y = yield x + 2
  console.log(2)
  return y
}

var g = gen(1)

当我们像上面那样调用生成器函数时,会发现并没有输出。这就是生成器函数与普通函数的不同,它可以交出函数的执行权(即暂停执行)。yield表达式就是暂停标志。

之前提到了生成器对象遵循迭代器协议,所以其实可以通过next方法执行。执行结果也是一个包含value和done属性的对象。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。

g.next() 
// 1
// { value: 3, done: false }
g.next() 
// 2
// { value: undefined, done: true }

for...of遍历

生成器部署了迭代器接口,因此可以用for...of来遍历,不用调用next方法

function *foo() {
  yield 1
  yield 2
  yield 3
  return 4
}

for (let v of foo()) {
  console.log(v)
}

// 1
// 2
// 3

yield*表达式

从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield表达式。yield后面只能跟迭代器,yield*的功能是将迭代控制权交给后面的迭代器,达到递归迭代的目的

function* foo() {
  yield 'a'
  yield 'b'
}

function* bar() {
  yield 'x'
  yield* foo()
  yield 'y'
}

for (let v of bar()) {
  console.log(v)
}

// x
// a
// b
// y

自动执行

下面是使用Generator函数执行一个真实的异步任务的例子:

var fetch = require('node-fetch')

function* gen () {
  var url = 'https://api.github.com/users/github'
  var result = yield fetch(url)
  console.log(result.bio)
}

上面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从JSON格式的数据解析信息。这段代码非常像同步操作,除了加上了yield命令。

执行这段代码的方法如下

var g = gen()
var result = g.next()

result
  .value
  .then(function (data) {
    return data.json()
  })
  .then(function (data) {
    g.next(data)
  })

上面代码中,首先执行Generator函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next方法。

可以看到,虽然Generator函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

那么如何自动化异步任务的流程管理呢?

Generator函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

  1. 回调函数。将异步操作包装成Thunk函数,在回调函数里面交回执行权。

  2. Promise对象。将异步操作包装成Promise对象,用then方法交回执行权。

Thunk函数

本节很简略,可能会看不太明白,请参考Thunk 函数的含义和用法

Thunk函数的含义:编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。

JavaScript语言是传值调用,它的Thunk函数含义有所不同。在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。

任何函数,只要参数有回调函数,就能写成Thunk函数的形式,可以通过一个Thunk函数转换器来转换。

Thunk函数真正的威力,在于可以自动执行Generator函数。我们可以实现一个基于Thunk函数的Generator执行器,然后直接把Generator函数传入这个执行器即可。

function run(fn) {
  var gen = fn()

  function next(err, data) {
    var result = gen.next(data)
    if (result.done) return
    result.value(next)
  }

  next()
}

function* g() {
  // ...
}

run(g)

Thunk函数并不是Generator函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制Generator函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise对象也可以做到这一点。

基于Promise对象的自动执行

首先,将方法包装成一个Promise对象(fs是nodejs的一个内置模块)。

var fs = require('fs')

var readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) reject(error)
      resolve(data)
    })
  })
}

var gen = function* () {
  var f1 = yield readFile('/etc/fstab')
  var f2 = yield readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

然后,手动执行上面的Generator函数。

var g = gen()

g.next().value.then(function (data) {
  g.next(data).value.then(function (data) {
    g.next(data)
  })
})

观察上面的执行过程,其实是在递归调用,我们可以用一个函数来实现:

function run(gen){
  var g = gen()

  function next(data){
    var result = g.next(data)
    if (result.done) return result.value
    result.value.then(function(data){
      next(data)
    })
  }

  next()
}

run(gen)

上面代码中,只要Generator函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。

co模块

co模块是nodejs社区著名的TJ大神写的一个小工具,用于Generator函数的自动执行。

下面是一个Generator函数,用于依次读取两个文件

var gen = function* () {
  var f1 = yield readFile('/etc/fstab')
  var f2 = yield readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

var co = require('co')
co(gen)

co模块可以让你不用编写Generator函数的执行器。Generator函数只要传入co函数,就会自动执行。co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(function () {
  console.log('Generator 函数执行完成')
})

co模块的原理:其实就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。如果数组或对象的成员,全部都是Promise对象,也可以使用co(co v4.0版以后,yield命令后面只能是Promise对象,不再支持Thunk函数)。

 

十八、async(异步)函数

是什么

async函数属于ES7。目前,它仍处于提案阶段,但是转码器Babel和regenerator都已经支持。async函数可以说是目前异步操作最好的解决方案,是对Generator函数的升级和改进。

怎么用

1)语法

async函数声明定义了异步函数,它会返回一个AsyncFunction对象。和普通函数一样,你也可以定义一个异步函数表达式。

调用异步函数时会返回一个promise对象。当这个异步函数成功返回一个值时,将会使用promise的resolve方法来处理这个返回值,当异步函数抛出的是异常或者非法值时,将会使用promise的reject方法来处理这个异常值。

异步函数可能会包括await表达式,这将会使异步函数暂停执行并等待promise解析传值后,继续执行异步函数并返回解析值。

注意:await只能用在async函数中。

前面依次读取两个文件的代码写成async函数如下:

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab')
  var f2 = await readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

async函数将Generator函数的星号(*)替换成了async,将yield改为了await。

2)async函数的改进

async函数对Generator函数的改进,体现在以下三点。

(1)内置执行器。Generator函数的执行必须靠执行器,所以才有了co函数库,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

var result = asyncReadFile()

(2)更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。co函数库约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以跟Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

3)基本用法

同Generator函数一样,async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

function resolveAfter2Seconds (x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x)
    }, 2000)
  })
}

async function add1 (x) {
  var a = resolveAfter2Seconds(20)
  var b = resolveAfter2Seconds(30)
  return x + await a + await b
}

add1(10).then(v => {
  console.log(v)  
})
// 2s后打印60

async function add2 (x) {
  var a = await resolveAfter2Seconds(20)
  var b = await resolveAfter2Seconds(30)
  return x + a + b
}

add2(10).then(v => {
  console.log(v)
})
// 4s后打印60

4)捕获错误

可以使用.catch回调捕获错误,也可以使用传统的try...catch。

async function myFunction () {
  try {
    await somethingThatReturnsAPromise()
  } catch (err) {
    console.log(err)
  }
}

// 另一种写法
async function myFunction () {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err)
  }
}

5)并发的异步操作

let foo = await getFoo()
let bar = await getBar()

多个await命令后面的异步操作会按顺序完成。如果不存在继发关系,最好让它们同时触发。上面的代码只有getFoo完成,才会去执行getBar,这样会比较耗时。如果这两个是独立的异步操作,完全可以让它们同时触发。

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])

// 写法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
原文地址:https://www.cnblogs.com/zjx304/p/10565104.html