ES6用法总结

ES6学习总结记录

[TOC]

前段时间自学前端时遇到了ES6新增部分,学习过后感觉对于构建大型项目还是挺有帮助的,特在此记录分享


let&const

const 与C++中的const关键字差别不大;

与 var 相比,let将变量作用域限定到了代码块中

此外,还需要注意的一点是,在全局使用 var 变量,会将其添加到上下文对象window中,而let不会

1
2
3
4
5
6
7
8
9
10
11
12
13
for(var i = 0; i < 10; ++i )
;
console.log(i); //输出10

for(let i = 0; i < 10; ++i )
;
console.log(i); //输出 undefined

var aloha = "pika!";
let deemo = "pika pi!";

console.log( window.aloha );//输出 pika!
console.log( window.deemo );//报错

模板字面量

  • 多行字符串 `demo` : 反引号中所有空白字符(空格、换行、制表符等)都属于字符串的一部分

    1
    2
    3
    4
    5
    let message = `Hello
    World!`;
    console.log(message);
    //Hello
    //World!
  • 占位符 ${ … } : 将表达式的值作为字符串的一部分输出

    1
    2
    3
    4
    let amount = 5;
    let price = 3.6;
    let message = `The total price is ${price * amount}!`;
    console.log(message);//The total price is 18!

函数形参默认参数:

可在指定默认参数后继续声明无默认值的参数,但此时需要传入undefine使用默认值

1
2
function demo(url = "/home", timeout = 2000, callback );
demo(undefined, undefined ,function(){});//使用默认参数

rest参数

JS的一个特点是,无论在函数定义里声明了多少形参,都可以传入任意数量的参数

  • arguments 对象(旧版):

    无需在定义中显式声明,可直接在函数中调用,其包含所有传入的参数,可使用下标直接遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function demo(pika){
    //若pika为true则遍历输出所有后续形参
    if(pika){
    for(let i = 1; i < arguments.length(); ++i)//从 1 开始是因为arguments[0]为形参pika
    {
    console.log(arguments[i]);
    }
    }
    }

    demo(true,2,3,4,5,6);//23456

    缺陷:

    1. 无法从函数形参列表看出函数是否被设计为接受任意数量的参数
    2. 使用多传入的形参时需要考虑下标起点
  • rest参数(ES6新增)

    ​ rest是一个数组,包含自它之后传入的所有参数,可直接下标遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function demo(pika, ...data){
    //若pika为true则遍历输出所有后续形参
    if(pika){
    for(let i = 0; i < data.length(); ++i)//从 0 开始是因为rest参数名只包含自它之后传入的所有参数
    {
    console.log(data[i]);
    }
    }
    }

    demo(true,2,3,4,5,6);//23456
    1. 要想使用rest,需要在形参名前加三个点,表明这是一个rest参数,用于获取函数多传进来的形参——解决缺陷1

    2. 无需考虑多传入的形参的下标起点问题,rest参数名只包含自它之后传入的所有参数

    3. 每个函数最多只能声明一个rest参数,且必须为最后一个参数

展开运算符

展开运算符为 ,可以将一个数组转换为各个独立的参数,也可去除对象的所有可遍历属性,需要注意与rest参数区分

示例:

  • 提取数组中值并作为形参

    1
    2
    3
    4
    5
    function sum (a,b,c){
    return a+b+c;
    }
    let arr = [1,2,3];
    sum(...arr);//等价于 sum(arr[0],arr[1],arr[2]);
  • 数组复制,合并

    1
    2
    3
    4
    5
    //将arr1数组赋值给arr2
    arr1=[1,2,3];
    arr2=[...arr1];//[1,2,3]
    //将arr1与arr2合并为arr3
    arr3=[...arr1, ...arr2];//[1,2,3,1,2,3]
  • 对象同理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let object1={
    name:"pika",
    age:"18"
    }
    let object2={
    ...object1,
    slogan:"wow"
    }
    console.log(object2);//{name:'pika',age:'18',slogan:'wow'}

对象字面量语法扩展

对面自变量是JS中创建对象的一种常用方法,ES6中增加了以下几种语法

  1. 属性初始值的简写

    在对象的一个属性和给其赋值的本地变量名称相同时,可不用写键值对,只写属性名即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //函数demo返回一个object
    //常用方法
    function demo(color, doors){
    var mineObject={
    color = color,
    doors = doors
    }
    return mineObject;
    }
    //ES6新增语法
    function demo(color, doors){
    var mineObject{
    color,
    doors
    }
    return mineObject;
    }

    当对象字面量里只有属性名称时,JS引擎会在可访问作用域中查找与其同名的变量,若能找到,则将其值赋予其同名属性

  2. 对象方法简写语法

    可省略冒号和function关键字(类似于C++对象内部定义函数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //一般方法
    let demo={
    name : "pika",
    pikachu : fuction(){
    ...
    }
    }
    //ES6新增语法
    let demo={
    name : "pika",
    pikachu(){
    ...
    }
    };

​ 使用简写语法创建的方法含有一个name属性,其值为函数名

console.log(demo.pikachu.name);// 输出为 pikachu

  1. 定义对象时属性名可为表达式

    1
    2
    3
    4
    5
    let attribute="name"
    let demo={
    ["first" + attribute]:"Pikachu",//firstname:"Pikachu",
    ["last" + attribute]:"Pika" //lastname:"Pika"
    }

    不局限于字符串拼接,任何表达式都可以,但都需要使用 []引用

解构赋值

在JS中,我们经常从某个对象或者数组中提取特定值付给变量,这种操作比较繁琐

1
2
3
4
5
6
7
8
9
10
11
12
let book={
title:"高等数学",
price:99,
category:{
id: 1;
name: "高等教育"
}
};
//提取对象中的值
let title = book.title;
let price = book.title;
let category = book.category.name;
对象解构

对象解构的语法形式是在一个赋值操作符的左侧放置一个对象字面量,如上例中

1
2
3
4
5
6
7
let book={
title:"高等数学",
price:99,
isbn:666
};
//提取对象中的值
let {title, price, isbn} = book;

在这段代码中,book.title 的值被赋予给声明的变量 title,book.price 被赋给变量price,此处注意声明的变量名称和相应属性名相同

若在提取值时已经声明了相应变量,则需要将语句使用小括号括起来:

1
2
3
4
5
let title;
let price;

{title, price} = book; //报错!
({title,price} = book);//正确

原因在于:
JS将开放的 {} 看作一个代码块,根据语法规则,代码块禁止放在赋值语句左侧。将其用()括起来,将块语句转为一个表达式,得以正常进行解构

使用解构赋值表达式时,若在对象中不存在与指定的变量名同名的属性或对象中与其同名的属性值为undefined,则该局部变量值会被赋为undefined,在这种情况下,我们可以考虑为其指定一个默认参数:

1
2
let {title, price, game} = book; //在book对象中不存在game属性,因此此处局部变量game值被赋为undefined
let {title, price, game = 'it takes two'} = book;//局部变量game值为'it takes two'

若要将对象属性赋给不与其同名的变量,使用 “属性名 : 变量名”的语法形式:

1
let {title : varFortitle, price : varForprice, isbn : varForisbn} = book;

title : varFortitle 的含义是,从 book 的 title 属性中读取值并将其赋给变量 varFortitle,需要注意要赋予的变量名称在冒号右边,而要读取的属性名在冒号左边

关于嵌套对象的解构赋值语法,在此先不进行介绍

解构赋值语句的值是 = 右边的对象,根据整个特点,常常在给函数传入对象作为参数时将对象属性的值赋给局部变量:

1
2
3
4
5
6
7
8
9
function demo(book){
console.log(book.isbn);
console.log(book.name);
...
}

let title;
let name;
demo({title, name} = book);//此时局部变量title的值为book.title的值,name同理,且将book对象传入了函数
数组解构

数组解构采用 [] 而非 {},由于数据结构的不同,数组并不需要考虑属性名的问题,因而语法相对简单一些

在数组解构语法中,变量值是根据数组中元素的顺序进行选取的,定义默认参数的方式也与对象类似

1
2
3
4
5
let arr = [1, 2, 3];
let [a, b, c] = arr; //a = 1, b = 2, c = 3
let [ , , c] = arr; //c = 3
let [a, b, c, d] = arr //a = 1, b = 2, c = 3, d = undefine
let [a, b, c, d = 4] = arr //a = 1, b = 2, c = 3, d = 4

箭头函数

ES6允许使用箭头”=>”定义函数,称为箭头函数。其语法多变,但都需要有函数参数,箭头,返回体组成

箭头函数语法

单一参数、函数体只有返回语句:

1
2
3
4
5
let demo = msg => msg;
//等价于
function demo(msg){
return msg;
}

多参数则需要在参数的两侧添加圆括号:

1
2
3
4
5
let demo = (user, msg) => `${user}, ${msg}`;//不记得的话复习一下一开始介绍的模板子向量
//等价于
function demo(user, msg){
return user + ", " + msg;
}

若函数无参/函数体为空:

1
2
3
4
5
6
7
8
9
10
let demo = () => "pika!";
//等价于
function demo(){
return "pika!";
}

//若要创建空函数,则用空花括号引起函数体
let demo = ()=> {};
//相当于
function demo(){};

若函数体有多条语句,需用花括号把函数体引起

1
2
3
4
5
6
7
8
9
let demo(a,b) = {
a -= b;
return a;
}
//等价于
function demo(a, b){
a-=b;
return a;
}

若函数返回一个对象字面量,则需将其用圆括号引起,这是为了将其与函数体区分开

1
let demo = (color, doors) => ( {color:color,doors:doors} );

箭头函数也可以和对象解构一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let demo = ( {name, isbn} ) => `The isbn of {$name} is {$isbn}.`;
//等价于
function demo(name, isbn){
return "The isbn of " name " is " isbn ".";
}
//调用时
let book = {
name : "高等数学",
isbn : "6666666"
};

let output = demo(book);
console.log(output); //输出 The isbn of 高等数学 is 6666666.

箭头函数与this

在介绍箭头函数中的this值之前,我们有必要回顾一下 JS 中 this 关键字的相关知识

this关键字

JS中的 this 关键字与某些高级语言的 this 指针\引用有所不同,JS中的this值并不一定指向对象本身,其指向会根据当前执行上下文的变化而变化,我们来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var singerName = "Troye";

function singer(){
console.log(this.singerName);
}

var demo = {
singerName : "Chainsmoker";
singer: singer;//函数是一等公民!
};

singer(); // Troye
demo.singer();// Chainsmoker
var hey = demo.singer;
hey(); // Troye

我们来分析上述代码

  • 执行 singer() 时,相当于执行windows.singer(),this指向的是window对象,而代码首行用var定义的 singerName 已自动成为了window对象的属性,因此this.singerName指向的是代码首行定义的全局变量,输出”Troye”

    值得注意的是,若首行中的singerName用let定义,则会输出undefined,这也与我们一开始所讲到的let的性质相吻合

  • 执行 demo.singer()时,this指针指向的是demo这个对象,于是其去demo属性中寻找singerName属性,输出为”Chainsmoker”

  • 执行 hey()时,虽然其是通过demo.singer直接赋值的,但在执行hey()时,当前的执行上下文对象为window,因此也输出了”Troye“

我们来看下一个例子

1
2
3
4
5
6
7
8
9
10
var demo = {
singerName : "Taylor",
singer(){
setTimeout(function(){
console.log( this.singerName );
},5000);
}
};

demo.singer();//输出为undefined

之所以会输出undefined,是因为在调用demo.singer()时,我们执行了setTimeout函数。而在五秒以后才开始执行我们在setTimeout中定义的匿名函数,此时执行上下文对象为window,而window对象并没有singerName这个属性,因此输出为undefined

为了解决this指向的问题,我们可以调用函数对象的bind ()方法,将this明确的绑定到一个对象上,例如在上述例子中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var singerName = "Troye";

function singer(){
console.log(this.singerName);
}

var demo = {
singerName : "Chainsmoker";
singer: singer;//函数是一等公民!
};

var hey = demo.singer.bind(singer);//将指针this与singer对象绑定
hey(); //输出为Chainsmoker


var demo_2 = {
singerName : "Taylor",
singer(){
setTimeout((function(){
console.log( this.singerName );
}).bind(this),5000);//传入bind()的this目前指向的是demo_2这个对象,相当于.bind(demo_2)
}
};
demo_2.singer();//输出为 Taylor

使用bind()方法实际上是创建了一个绑定函数,该函数的this被绑定到了传入的对象

我们可以使用箭头函数来避免创造绑定函数

箭头函数的this

箭头函数中没有 this 绑定,需要通过查找作用域确定this的值。若箭头函数被非箭头函数包含,其this值与最近的包含它的非箭头函数的this值相同;若箭头函数为全局函数,则其this的值会指向全局对象(在JS中通常为window)

适时使用箭头函数会使代码变得简洁。如在之前的setTimeout的例子中,匿名函数用箭头函数形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
var demo = {
singerName : "Taylor",
singer(){
/*setTimeout(function(){
console.log( this.singerName );
},5000);*/
setTimeout(()=>{ //如果这里有疑问,就需要去复习匿名函数了
console.log( this.singerName );
},5000);
}
};

demo.singer();//输出为Taylor

总的来说,箭头函数中的this取决于其外部非箭头函数的this的值

使用箭头函数注意点
  1. 箭头函数无 this, super, arguments 和 new.target绑定。其 this, super, arguments 和 new.target值取决于其外部最近一层非箭头函数决定

  2. 箭头函数不能被用作构造函数,即无法通过new来调用

  3. 没有原型。由于箭头函数无法通过new调用,因此也没有为其构建相应原型的需求,即其无prototype属性

  4. 在函数生命周期内,this的值不可改变

  5. 不支持arguments对象,但仍然可以通过已命名的形参以及rest参数来访问函数的参数

Symbol

ES6引进了一种新的数据类型——“Symbol”,表示独一无二的值

Symbol值的创建

一个具有Symbol类型的值被称为“符号类型值”,在JS中通过调用Symbol()函数动态生成一个匿名的、唯一的值,它没有字面量的写法

具体如下:

1
2
3
4
5
6
let a=Symbol();
let b=Symbol();

console.log(a==b); //false
console.log(a); //Symbol()
console.log(b); //Symbol()

由于Symbol函数会动态生成唯一值,因此a与b的值并不相等。Symbol是原始值,不能使用new Symbol() 创建Symbol值,且每一个Symbol实例是唯一的且不可改变的

我们发现a与b在输出时均为Symbol(),即使两者值并不相同。因此我们可以通过在Symbal函数中添加字符串参数来对其实例增加描述,这个描述不可用于进行属性访问,且并不会对实例本身有任何影响,仅仅是为了方便调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = Symbol("pika");
let b = Symbol("pika pi!");

console.log(a);
console.log(b);

let d = a;
console.log(a == d); //将a值赋给了d

let c = Symbol("pika");
console.log(a == c); //a与c的Symbal描述一致

//输出为:
Symbol(pika)
Symbol(pika pi!)
true
false

由上可验证,不同的Symbol,即使有着相同描述,实例值本身并不相同;直接将Symbol值赋给另一个变量,两者值是相同的

关于类型转换

Symbol值比较特殊,没有与其逻辑等价的值,JS无法将其自动进行类型转换。我们只能通过调用其toString方法将其转换为字符串,即将Symbol()转换为”Symbol()”。除此以外,在尝试与其他类型值拼接/运算时,则会报错

有一个例外是,Symbol值可以参与逻辑运算。因为JS将非空值都看为true,因此Symbol值在布尔运算中永远为true

作为属性名

Symbol类型唯一比较合理的用法是用变量存储Symbol值,再用这个变量存储的值作为属性名来创建对象的属性,由于每一个Symbol值都不相等,因此对象的属性名永远不用担心重复的问题,这也是引入Symbol的初衷之一

考虑这样一种情况:两个不同的库想要向一个对象添加基本数据,可能它们都想在对象上设置某种标识符,假设要设置各自的ID。如果简单地使用诸如ID作为键,这样存在一个巨大的风险,就是多个库可能使用相同的键

1
2
3
4
5
6
function lib1tag(obj) {
obj.id = 42;
}
function lib2tag(obj) {
obj.id = 369;
}

通过使用 Symbol,每个库可以在实例化时生成所需的 Symbol。然后用生成 Symbol 的值做为对象的属性:

1
2
3
4
5
6
7
8
const library1property = uuid(); // random approach
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = "LIB2-NAMESPACE-id"; // namespaced approach
function lib2tag(obj) {
obj[library2property] = 369;
}

需要注意的是,Symbol作为对象属性名,相应值是不能使用点运算符获取的,只能使用中括号来获取,且该属性是公有的而非私有

要读取一个对象的Symbol属性,可以通过Object.getOwnPropertySymbols()和Reflect.ownKeys()方法得到

共享的Symbol

如果想在不同的代码片段使用同一个Symbol值,可以通过使用 Symbol.for() 方法来实现,但与Symbol()不同的是,它要求必须给定一个字符串参数作为标识符以及描述,以此来辨别Symbol值

ES6提供了一个可以随时访问的全局Symbol注册表,当调用Symbol.for(‘demo’)时,JS会先去全局注册表中寻找标识符为 ‘demo’ 的值,如果找到了则返回这个值,如果没找到则创建一个以‘demo’为标识符的Symbol值,并将其注册到Symbol注册表中

1
2
3
4
5
6
7
let a = Symbol.for("pika");
let b = Symbol.for('pika');

let c = Symbol('pika');

console.log(a == b); //true
console.log(a == c); //false

由此可见,Symbol.for()与Symbol()的区别在于:前者会将创建的Symbol注册到全局Symbol注册表中,当再次使用相同的key调用Symbol .for()时,会返回同一个Symbol值;而后者无论key值相同与否,都会创建一个新的、与已有的Symbol都不同的值

绝大多数面向对象的编程语言支持类和类继承的特性,而 JS 却不支持这些特性,在 ES6 之前,我们需要混合使用构造函数和原型prototype来模拟定义一个对象并实现类的继承,这相当繁琐。在ES6中,引入了class关键字,使类的定义更接近C++、Java的语法

类的定义

如要定义一个Car类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class car{
//构造函数
constructor(sColor,iDoors){
this.color = sColor;
this.doors = iDoors;
},
showColor(){
console.log(this.color);
}
}

let mCar = new car('red',4);
mCar.showColor(); //red

需要与C++对象区分的一点是,通过方法(可以理解为成员函数,如构造函数方法或者其他方法)创建的属性称为自由属性,自由属性只能为对象实例拥有,而不会出现在原型上。如上例中,color 和 doors 属性只会出现在mCar上,对象原型car本身并不含有这些属性。本例中写的方法 showColor 实际上是car.prototype的一个方法

类也可以用表达式创建,但本质上并没有任何区别,在此不多赘述

访问器属性

访问器属性是通过关键字 get、set 跟一个空格后加一个标识符实现的,实际上是为了便于给某个自由属性定义取值和设值函数,在使用是直接以属性访问的方式使用。与自由属性不同的是,访问器属性需要在原型中直接创建

例如为上例中的car设置访问器属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class car{
//构造函数
constructor(sColor,iDoors){
this.color = sColor;
this.doors = iDoors;
},
//只读
get desc(){
return 'The color of the car is ' + this.color;
}
get color(){
return this.color
}
get color(value){
this.color = value;
}
}

let iCar = ner car('red', 4);

console.log (iCar.color);//'red'
console.log (iCar.desc); //The color of the car is red
iCar.color = 'blue'; //若未定义get访问器属性,则自由变量color无法通过此方法改变值
console.log (iCar.color);//'blue'
静态方法关键字 static

ES6中引入的 static 关键字用于类方法定义时与C++中的 static 关键字较为类似,使用static关键字定义的静态方法只能通过对象原型(也就是类名)调用,无法通过对象实例调用

值得注意的是ES6中并未提供静态属性,因此无法在定义实例前加关键字static

类的继承

ES6提供了extends关键字用于更轻松的实现类的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person{
constructor(name){
this.name = name;
}

work(){
console.log("working...");
}
}

class Student extend Person{
constructor(name, grade){
super(name);//调用父类的构造函数
this.grade = grade;
}
//可重写继承于父类的方法,这会将其覆盖
work(){
console.log("studying...");
}
}

若派生类显式定义了构造函数,则必须通过调用 super 来调用父类的构造函数,而且必须在访问this之前调用。若派生类未显式定义构造函数,则会默认自动调用父类的构造函数并向其传参

派生类可以重写继承于父类的方法,倘若要在重写的方法中调用父类的此方法,可以通过调用super来实现,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//在子类work方法中调用父类work方法
class Student extend Person{
constructor(name, grade){
super(name);//调用父类的构造函数
this.grade = grade;
}
//可重写继承于父类的方法
work(){
super.work();//调用父类方法 work
console.log("studying...");
}
}

let stu = Student("JJy",59);
stu.work(); //working...studying...

模块

在ES5及早期版本中,JS中并未有模块体系,导致无法将一个引用拆分成不同部分,再将各部分组合起来使用,为此,JS社区中制定了一些解决方案,如CommonJS、AMD等等。ES6 在语言标准的层面上实现了模块功能,而且实现的相当简单,完全可以取代目前主流的解决方案,并已成为浏览器和服务器通用的模块解决方案

一个模块通常是一个单独的JS文件,文件中的变量,函数除非被导出,否则无法被外部使用

export关键字

使用 export 关键字放置在需要导出给其他模块的变量、函数或类声明前,以将其导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//导出函数
export function Pika(){
return "Pika pi!";
}
//导出变量、类、对象
export var a;
export const b = 100;
export let c;

export class car{
constructor(name){
this.name = name;
}
}

export var pet = {
petName :'pakachu'
}

我们也可以在模块尾部使用 export{} 集中导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Pika(){
return "Pika pi!";
}

var a;
const b = 100;
let c;

class car{
constructor(name){
this.name = name;
}
}

var pet = {
petName :'pakachu'
}

//可以集中导出
export{Pika,a,b};
//也可以逐个导出
export {c};
//可以用 as 定义导出后的名称
export {car as Senna};//car是模块内部名,Senna是模块外部名
//一个模块可且仅可导出一个值作为默认值,需使用关键字 default
//导出默认值无需花括号
export default pet;
  • 在需要导出的部分较多时,使用第二种方法更加清晰
  • 导出时可以集中导出也可以逐步导出,集中导出需要使用逗号分隔
  • 导出时可指定导出后的名称,用 as 关键字来指定
  • 一个模块可且仅可导出一个值作为默认值,需使用关键字 default ,导出默认值时无需花括号
import关键字

使用 import 关键字来导入外部模块导出的功能,import 语句包含两部分:要导入部分的标识符以及来源:

1
2
3
4
5
6
7
//假设要导入上例中模块
import { a, b, c } from "./module"; //可同时导入多个值
import { Senna } from "./module"; //导入的名称是 Senna 而不是 car
import pet from "./module"; //默认值的导入同样不需要花括号
import { Pika as Pikachu} from "./module";//同样使用 as 来定义导入后使用名称Pikachu

Pikachu(); //使用Pikachu而不是Pika

需要注意的是,导入和导出一般在模块顶部进行

Promise

JS 引擎是基于单线程事件循环的概念构建的,它采用任务队列的方式进行,JS 执行异步调用的传统方式是时间和回调函数,随着应用的复杂化,时间和回调函数要满足一个开发者的需求,往往会变得比较复杂,为此,ES6 给出了 Promise 这一更加简单的异步编程解决方案

基本用法

一个Promise对象可以通过 Promise 构造函数进行创建,这个构造函数只接受一个参数:包含用来初始化 Promise 代码的执行函数,在执行函数中包含需要异步执行的代码,其只接受两个参数 :resolve()reject() 函数,这两个函数由JS引擎提供,我们不需要去编写。当异步代码执行成功时,调用 resolve() 函数;失败时,则调用 **reject()**函数

1
2
3
4
5
6
7
8
9
10
11
var demo = new Promise(function (resolve, reject) {
//开启异步操作
setTimeout(function () {
try {
var c = 3 / 1;
resolve(c);
}catch(exp){
reject(exp);
}
} ,3000);
});

在执行器函数中模拟了一个异步调用,在3秒后执行除法操作,如果未抛出异常则调用 resolved 函数,反之则调用 reject 函数

每一个Promise都会经历一个短暂的声明周期:先是处于运行中(pending)的状态,此时代表着执行函数内的操作尚未完成,因此它也叫做未处理(unsettled)状态。一旦异步操作执行结束,便变为已处理(settled)状态,此时根据执行结果进入以下两个状态之一:

  1. fufilled : Promise异步操作成功完成
  2. rejected : 由于程序错误或者其他原因, 异步操作未能完成

一旦Promise状态改变,就不会再变,任何时候都能得到这个结果

在Promise状态改变后,我们通过调用 Promise 对象的then方法对不同的状态作相应的处理

then方法接受两个函数作为参数,第一个是 Promise 变为 fufilled 状态时调用的函数,所有传入resolve中的形参都会被自动传递给这个函数,第二个是 Promise 变为 rejected 状态时调用的函数,所有传入reject中的形参都会被自动传递给这个函数。我们接着上面的例子,完善相应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var demo = new Promise(function (resolve, reject) {
//开启异步操作
setTimeout(function () {
try {
var c = 3 / 0;
//throw new Error("error!")
resolve(c);
}catch(exp){
reject(exp);
}
} ,3000);
});
//调用then方法
demo.then(function(value){
console.log('The value is '+ value);//这里的value值是传入resolve中的c的值,也就是3
},function(errhhh){
console.log(errhhh.message);//如果执行函数中并未注释掉throw语句,则此处errhhh为catch到的异常
});

then 的两个参数都是可选的,如果只在失败后进行相应处理,可以给 then 方法的第一个参数传入null

Promise 对象还有一个 catch方法,这个方法只用于在异步执行失败后进行处理,等价于上文中 then 方法第一个函数传入null,在实际应用中,更多将 thencatch 方法结合起来使用,并通过箭头函数使代码更加简洁易读。如上例可改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var demo = new Promise(function (resolve, reject) {
//开启异步操作
setTimeout(function () {
try {
var c = 3 / 0;
throw new Error("error!")
resolve(c);
} catch (exp) {
reject(exp);
}
}, 3000);
});

demo.then((value) => console.log('The value is ' + value))
.catch((errhhh) => console.log(errhhh.message)); //这里可以采用方法链调用,
//说明then返回值是一个Promise对象(实际上catch也是)
//输出:error!
Promise的方法链调用

Promise和其他对象一样,也支持方法链调用,如上述代码所示。每次调用 then、catch 方法时,实际上是创建并返回了一个Promise,因此可以将Promise串联匿名使用。在串联调用是,只有当前一个Promise对象完成或者被拒绝时,后一个Promise对象才会被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var demo = new Promise(function (resolve, reject) {
//调用setTimeout模拟异步操作
setTimeout(() => {
let intArr = new Array(20);
for (let i = 0; i < 20; ++i) {
intArr[i] = parseInt(Math.random() * 20, 10);
}
//生成随机数组后调用resolve
resolve(intArr);
}, 3000);

console.log("输出有序数组");
});

demo.then((randomArr) => {
randomArr.sort((a, b) => a - b);//递增排序
return randomArr; //通过return将值传给后续即将被调用的Promise
}).then((orderedArr) => console.log(orderedArr));

上述代码的输出结果如下:

1
2
3
4
输出有序数组
[
0, 0, 0, 1, 1, 3, 3, 7, 8, 8, 9, 9, 10, 12, 13, 13, 14, 16, 16, 17
]

说明:

  1. 在执行函数中的20个随机数生成完毕后,调用 resolve 函数并将数组传入。随后 then 方法的完成处理函数被调用,对数组进行排序,完成处理函数将排好序的数组通过关键字 return 传入下一个Promise,而第一个then 方法结束后返回了一个Promise对象。紧接着该对象的 then 方法被调用,其完成处理函数接收由上一个 then 方法排好序的数组,并将其输出

  2. Promise链式调用时,有一个重要特性是可以通过在完成处理函数中指定一个返回值(如上例中的 return randomArr),从而沿着Promise链传递数据

在完成处理函数或拒绝处理程序中也可能出现一个错误,通过链式调用可以很好的捕获这些错误代码,甚至为其提供错误处理程序。

假设上例中排序过程中可能出现错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var demo = new Promise(function (resolve, reject) {
//调用setTimeout模拟异步操作
setTimeout(() => {
let intArr = new Array(20);
for (let i = 0; i < 20; ++i) {
intArr[i] = parseInt(Math.random() * 20, 10);
}
//生成随机数组后调用resolve
resolve(intArr);
}, 3000);

console.log("输出有序数组");
});

demo.then((randomArr) => {
randomArr.sort((a, b) => a - b);
throw Error("Sorting Error!"); //手动抛出异常模拟完成处理函数运行过程中抛出异常
return randomArr;
}).then((orderedArr) => console.log(orderedArr),
(errmsg) => console.log(errmsg.message));

输出为:

1
2
输出有序数组
Sorting Error!

需要注意的是,在Promise中如果没有使用 catch 函数指定错误处理函数,那么Promise对象抛出的错误将不会被传递到外层代码

总的来说,通过链式调用,前一个Promise在状态确定后执行的完成处理函数或错误处理函数相当于后一个Promise对象的执行函数!!

创建已处理的Promise对象,

有时候我们想将一个现有的对象转换为一个Promise对象,这时我们可以通过调用 Promise.resolve() 方法来实现,其参数分为下列三种情况:

  1. 若参数本身就是一个Promise对象,则直接将其返回

  2. 若参数是一个具有 then 方法的对象(我们叫它 thenable 对象),则立即执行这个 thenable 对象的 then 方法,再根据其执行是否成功创建并返回 fulfilled 态或者 reject 态的Promise对象(类似于方法链调用的前后关系——前一个Promise在状态确定后执行的完成处理函数或错误处理函数相当于后一个Promise对象的执行函数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var demo = {
    then(resolve, reject) {
    resolve("pika!");
    }
    }

    var a = 10; //a不是Promise对象

    a = Promise.resolve(demo);
    a.then((value) => console.log(value)); //a可以调用then方法的完成处理函数,其是一个fulfilled态的Promise对象

    //输出为 pika!
  3. 若参数为空或为基本数据类型,或是不具有 then 方法的对象,那么会返回一个 fulfilled 状态的Promise对象,并将参数传递给返回的Promise对象的 then 方法

    1
    2
    3
    4
    5
    6
    var a = 10; //a不是Promise对象

    a = Promise.resolve("hello!");
    a.then((value) => console.log(value)); //a可以调用then方法的完成处理函数,其是一个fulfilled态的Promise对象

    //输出为 hello!

通常来说,如果不知道一个值value是否为Promise对象,可以不管三七二十一,直接调用 Promise.resolve(value) ,这样子就能把value当作Promise对象使用了

Promise.reject() 也会返回一个新的Promise对象,与Promise.resolve 十分类似,唯一不同的是若参数为第三种情况会默认返回状态为 rejected 的Promise对象,接收错误信息并传递给指定好的错误处理函数

1
2
3
4
5
6
var a = 10; //a不是Promise对象

a = Promise.reject("hello!");
a.catch((value) => console.log(value)); //a可以调用then方法的完成处理函数,其是一个fulfilled态的Promise对象

//输出为 hello!
响应多个Promise

如果需要等待多个Promise处理完成后再进行下一步操作,可以调用 Promise.all() 方法。该方法接受一个参数并返回一个新的Promise对象,参数是一个包含多个Promise的可迭代对象(例如数组)。返回的Promise对象只有当参数中的所有Promise对象全成功时才会触发成功态,一旦参数中有任何一个Promise对象失败,则返回的Promise对象触发失败。这个新的Promise对象会在触发成功时将参数中全部Promise对象返回值以数组的形式传给其完成处理函数,顺序与参数中可迭代对象中的Promise顺序保持一致;相应地,在触发失败时会将参数列表中第一个触发失败的Promise的错误信息返回给其失败处理函数。

Promise.all() 方法适合处理Promise对象集合,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let promise_1 = Promise.resolve(5); 
let promise_2 = 10; //不是Promise对象
let promise_3 = new Promise((resolve,reject)=>setTimeout(resolve, 500, "Hello!"));//模拟异步,执行成功
let promise_4 = new Promise((resolve,reject)=>setTimeout(()=>{ //模拟异步,执行失败
try{
throw new Error("Error!");
}catch(ex){
reject(ex);
}
},3000));

Promise.all([promise_1,promise_2,promise_3]).then(
value=>console.log(value));

Promise.all([promise_1,promise_3,promise_4]).then(value=>console.log("Hello!"))
.catch(errmsg=>console.log(errmsg.message));

输出:

1
2
[ 5, 10, 'Hello!' ]
Error!

由上可知,如果一个非Promise对象传入了 Promise.all() 中,其会忽略该值,也会将其放入返回的数组的相应位置(假设全部Promise对象都成功)

ES6还提供了Promise.race() 方法,与 Promise.all() 方法不同的是,Promise.race() 方法只要有一个Promise成功或者失败,则返回一个相应状态的Promise对象,并将对应的信息传入相应的处理函数,因此其返回对象的状态取决于参数列表最早处理完成的Promise对象

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise_3 = new Promise((resolve,reject)=>setTimeout(resolve, 500, "Hello!"));//延时500ms
let promise_4 = new Promise((resolve,reject)=>setTimeout(()=>{ //延时3000ms
try{
throw new Error("Error!");
}catch(ex){
reject(ex);
}
},3000));

Promise.race([promise_3,promise_4]).then(value=>console.log(value))
.catch(errmsg=>console.log(errmsg.message));
//输出 Hello
//若将promise_4 中异步操作延迟调为<500ms,则输出 Error

async函数

async函数是在ES8标准中引入的,async函数是使用 async关键字声明的函数,在其内部可以使用await关键字,表明紧跟在后面的表达式需要执行,其后的代码需要等待执行完毕后再继续执行

使用async函数和await关键字,可以更简洁的写出基于Promise的异步行为

基本用法

async函数会返回一个Promise对象,如果其显式返回值不是一个Promise对象,则会将其隐式包装在Promise中并返回

1
2
3
4
5
6
7
async function demo(){
return "Hello";
}

console.log(demo);
console.log(demo());
demo().then(value=>console.log(value));

输出:

1
2
3
[AsyncFunction: demo]
Promise { 'Hello' }
Hello

async函数内部return返回的值,会成为then()方法中回调函数的参数

async函数中可以有 await 表达式,当遇到它时,函数会先暂时停止执行,等待 await 后触发的异步操作完成后,再继续async函数的执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function demo(){
return new Promise(resolve=>
setTimeout(() => {
console.log("延时任务");
resolve();
}, 1000));
}

async function asyDemo(){
console.log("即将开始");
await demo();
console.log("异步处理完成");
}

asyDemo();

输出:

1
2
3
即将开始
延时任务
异步处理完成

await 关键字后面,可以是Promise对象或者原始类型值,若为原始类型值,则会自动转为resolved状态的Promise对象

一个 async 函数的函数体可看作是由0或多个 await 表达式分成的代码块,每个代码块代表一个异步执行操作(await后跟着的)及同步执行代码(两个await之间的代码)

await和并行任务执行

在async函数中可以有多个await任务,如果这多个任务之间没有明确要求执行顺序,可以使用**Promise.all()**方法来并行执行多个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function demo(){
return new Promise(resolve=>
setTimeout(() => {
console.log("延时任务--1s");
resolve("任务1执行完成");
}, 1000));
}
function demo_2(){
return new Promise(resolve=>
setTimeout(() => {
console.log("延时任务--2s");
resolve("任务2执行完成");
}, 2000));
}

async function asyDemo(){
console.log("即将开始");
await Promise.all([demo(),demo_2()])
console.log("异步处理完成");
}

asyDemo();

输出为:

1
2
3
4
即将开始
延时任务--1s
延时任务--2s
异步处理完成

我们来看一个await表达式的值是什么,将上例中的asyDemo函数改为:

1
2
3
4
5
6
async function asyDemo(){
console.log("即将开始");
console.log(await Promise.all([demo(),demo_2()]));
console.log("异步处理完成");
}

输出为:

1
2
3
4
5
即将开始
延时任务--1s
延时任务--2s
[ '任务1执行完成', "任务2执行完成" ]
异步处理完成

此处表达式的值是**Promise.all()**返回的值,说明表达式的值为其后跟的Promise对象异步执行完毕后返回的信息

使用async函数重写Promise链

Promise方法链实质上是多个Promise对象连续执行的一种简写形式,将其拆解为一个个独立的Promise对象,在async函数中对Promise对象单独或部分集中(使用Promise.all()或Promise.race()聚合)使用await表达式,即可将Promise链改写为async函数形式,在此不再赘述

错误处理

如果async函数内部抛出错误,其返回的Promise对象会被置为reject状态,而抛出的错误信息可以通过**catch()**方法的回调函数接收到,代码如下:

1
2
3
4
5
6
7
8
9
async function asyDemo(){
await new Promise((resolve,reject)=>{
throw new Error("错误!");
})
return "Hello";
}

asyDemo().then(value=>console.log(value))
.catch(errmsg=>console.log(errmsg.message));

输出:

1
错误!

在上述代码中,await后面的Promise对象抛出错误对象,async 函数返回reject状态的Promise对象,其catch()函数被调用,参数即为抛出的错误对象

若想要在内部的Promise出错时不返回错误对象,可将await写入try/catch语句块中,在 async 内部进行相应错误处理,如

1
2
3
4
5
6
7
8
9
10
11
12
13
async function asyDemo(){
try{
await new Promise((resolve,reject)=>{
throw new Error("错误!");
})
}catch(ex){
//错误处理
}
return "Hello!";
}

asyDemo().then(value=>console.log(value))
.catch(errmsg=>console.log(errmsg.message));

输出:

1
Hello!

如果有多个await,可以统一放入try/catch语句块中


ES6用法总结
http://example.com/2022/03/24/ES6用法总结/
作者
Pikachudy
发布于
2022年3月24日
许可协议