欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ECMAScript6 新的数据结构、Class语法、Proxy和Promise,看完我写的ES6,马上会的,你就是高手

程序员文章站 2022-06-13 16:12:15
...

ES6

我们已经了解了以下复杂的数据结构:

  • 存储带键的数据(keyed)集合的对象。
  • 存储有序集合的数组。

但这还不足以应对现实情况。这就是为什么存在 MapSet

新的数据结构

Map

Map 是一个带键的数据项的集合,就像一个 Object 一样。 但是它们最大的差别是 Map 允许任何类型作为键(key)。

它的方法和属性如下:

  • new Map() —— 创建 map。
  • map.set(key, value) —— 根据键存储值。
  • map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
  • map.has(key) —— 如果 key 存在则返回 true,否则返回 false
  • map.delete(key) —— 删除指定键的值。
  • map.clear() —— 清空 map。
  • map.size —— 返回当前元素个数。

例如:

// 创建一个map数据集合
let map = new Map();
var obj = {};

// 往里面添加内容
map.set("1", "string"); 	// 字符串键
map.set(1, "number"); 		// 数字键
map.set(true, "boolean"); 	// 布尔值键
map.set(obj, "object"); 	// 对象键

console.log(map.get(1)); 	// number 
console.log(map.get("1"));  // string
// map 是根据类似于 === 的算法来比较是否是键名的,但是NaN也可以被识别
console.log(map.get(obj)); 	// object

console.log(map.size); 		// 3

每一次 map.set 调用都会返回 map 本身,所以我们可以进行“链式”调用:

map.set('1', 'str1')
  .set(1, 'num1')
  .set(true, 'bool1');

Map 迭代

如果要在 map 里使用循环,可以使用以下三个方法:

  • map.keys() —— 遍历并返回所有的键(returns an iterable for keys),
  • map.values() —— 遍历并返回所有的值(returns an iterable for values),
  • map.entries() —— 遍历并返回所有的实体(returns an iterable for entries)[key, value]for..of 在默认情况下使用的就是这个。

例如:

let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// 遍历所有的键(vegetables)
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// 遍历所有的值(amounts)
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// 遍历所有的实体 [key, value]
for (let entry of recipeMap) { // 与 recipeMap.entries() 相同
  alert(entry); // cucumber,500 (and so on)
}

除此之外,Map 有内置的 forEach 方法,与 Array 类似:

// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

与普通对象 Object不同点:

  • 任何类型值都可以作为键。
  • 有其他的便捷方法,如 size 属性。

Set

Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。

它的主要方法如下:

  • new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
  • set.add(value) —— 添加一个值,返回 set 本身
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 返回元素个数。

它的主要特点是,重复使用同一个值调用 set.add(value) 并不会发生什么改变。这就是 Set 里面的每一个值只出现一次的原因。

var s = new Set();
// [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

console.log(s)
// [2 3 5 4]

Set迭代

Map 中用于迭代的方法在 Set 中也同样支持:

  • set.keys() —— 遍历并返回所有的值(returns an iterable object for values),
  • set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map
  • set.entries() —— 遍历并返回所有的实体(returns an iterable object for entries)[value, value],它的存在也是为了兼容 Map

我们可以使用 for..offorEach 来遍历 Set:

let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set){
    console.log(value);
}

// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
  	console.log(value);
});

**Set实现数组去重 **

var arr = [1,2,3,4,5,3,3,2,2,1];
var newArr = [...new Set(arr)];
console.log(newArr); // [1, 2, 3, 4, 5]

class

在现代 JavaScript 中,有一个更为高级的类构造方式,它引入了对于面向对象编程很有用的功能。

Class语法

class MyClass {
  prop = value; // 属性
  
  constructor() { ... } // 构造器
  method1() { ... } // 方法
  method2() { ... }
  method3() { ... }
  ...
}

然后通过 new MyClass() 来创建具有上述列出的所有方法的新对象。

通过 new 关键词创建的对象会自动调用 constructor() 方法,因此我们可以在 constructor() 里初始化对象。例如:

class Person {
	constructor(name){
        this.name = name;
    }
    sayName(){
        console.log(this.name);
    }
}

var person = new Person("李白");
person.sayName();

注意:类方法之间没有逗号。

解析:在 js 中,类是一个函数,如上例 typeof Person 值是 functionclass Person {...} 构造器内部为我们做了什么:

  1. 创建一个以 Person为名称的函数,这是类声明的结果。该函数的代码来自于 constructor 方法(如果没有写,那么它就假定为空)
  2. 储存类中的方法,例如 Person.prototype 中的 sayName

与之前的构造函数不同的是

  1. 类不能像构造函数那样直接调用,必须通过 new 来调用
  2. 类方法不可枚举
  3. 类的属性不会共享,只有方法共享
  4. 类总是使用 use strict,在类构造中的所有代码都将自动进入严格模式

静态属性

我们可以把一个属性或方法赋值给一个类方法,而不是赋给它的 "原型对象"。这样的属性和方法我们称为静态的

在类里面,他们在前面添加 “static” 关键字,例如:

class User {
  static n = 10;
  static staticMethod() {
    console.log(this === User);
  }
}

User.n = 10;
User.staticMethod(); // true
var user = new User();
console.log(user.n); // undefined
console.log(user.staticMethod); // undefined

静态的属性和方法可被继承

计算属性

类似与对象字面量,类包括 getters/setters,计算属性(computed properties)等。

class User {
    constructor(name){
        this.name = name;
    }
    get name(){
        return this._name;
    }
    set name(v){
        if(v.length < 4){
            alert("Name is too short.");
        }
        this._name = v;
    }
    
    [this.name + "hello"](){
        console.log("hello");
    }
}

let user = new User("李白"); // Name is too short
let userMore = new User("libai");

类表达式

如函数一样,类可以在另一个表达式中定义,传递(passed around),返回(returned),调用(assigned)等。例如:

var User = class {
  sayHi() {
    alert("Hello");
  }
};

类似于命名函数表达式(Named Function Expressions),类表达式可能也可能没有名称。

如果类表达式有名称,它仅在类内部可见:

// “命名类表达式”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
var User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass 仅在其内部可见
  }
};

new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容

alert(MyClass); // 错误,MyClass 在外部不可见
function makeClass(phrase) {
  // 声明并返回类
  return class {
    sayHi() {
      alert(phrase);
    };
  };
}

// 创建新类
let User = makeClass("Hello");

new User().sayHi(); // Hello

类继承

如果一个类要继承自另一个类,我们需要在 {..} 前指定 extends 和父类。例如:

class Animal{
	// 构造函数
	constructor(name){
		this.name = name;
	}
	run(){
		console.log("running ...");
	}
}

// 通过指定“extends Animal”让 Dog 继承自 Animal
class Dog extends Animal{
	skill(){
		console.log("lookHouse ...");
	}
}

var dog = new Dog("旺财");
dog.run();
console.log(dog.name);

extends 允许后接任何表达式

类语法不仅可以指定一个类,还可以指定 extends 之后的任何表达式。

function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

通常来说,我们不希望完全替换父类的方法,而是希望基于它做一些调整或者功能性的扩展。我们在我们的方法中做一些事情,但是在它之前/之后或在执行过程中调用父类方法。

为此,类提供了 "super" 关键字。

  • 执行 super.method(...) 调用父类方法
  • 执行 super(...) 调用父类构造函数(只能在子类的构造函数中运行)

继承类的 constructor 必须调用 super(...),并且一定要在使用 this 之前调用。

例如,让 Dog 拥有自己的颜色,然后添加一个自己的跑动的方法:

class Animal{
	// 构造函数
	constructor(name){
		this.name = name;
	}
	run(){
		console.log("running ...");
	}
}

// 通过指定“extends Animal”让 Dog 继承自 Animal
class Dog extends Animal{
    constructor(name, color){
      	// 如果想要书写自己的构造函数, super一定要有
        super(name); // 调用父类Animal的构造函数
		this.color = color;
	}
	skill(){
		console.log("lookHouse ...");
	}
    run(){
        super.run(); // 调用父类Animal的跑方法
        console.log("添加的独特的跑步方法");
    }
}

var dog = new Dog("旺财", "yellow");
dog.run();
console.log(dog);

类作业

创建一个继承自 Clock 的新的类 ExtendedClock,并添加参数 precision — 每次输出结果的间隔的毫秒数,默认是 1000(1 秒)。

  • 你的代码应该在 extended-clock.js 文件里。
  • 不要修改原有的 clock.js。请扩展它。
  • 面向对象案例
<script>
    <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="https://cdn.bootcss.com/jquery/1.10.1/jquery.min.js"></script>
    <style>
        .clearfix:after {
            content: " ";
            display: block;
            height: 0;
            clear: both;
        }
        .clearfix {
            zoom: 1;
        }
        *{
            list-style: none;
        }
        .tab{
            border-bottom: 1px solid red;
        }
        .tab li{
            float: left;
            padding: 10px;
        }
        .tab li:hover{
            color: red;
        }
        .tab .on{
            color: red;
        }
        .box  li{
            display: none;
        }
        .box .on{
            display: block;
        }
    </style>
</head>
<body>
    <div id="main">
        
    </div>
    <div id="main1">

    </div>
    <script>
        class Tab {
            constructor(id , arrs){
                this.id = id;
                this.arrs = arrs;
                this.init();
            }
            init(){
                this.initBox(); // 初始化 容器
                this.initTop(); // 初始化 tab头部栏
                this.initBot(); // 初始化 tab底部栏
            }
            initBox(){
                var str = `<div class="tab clearfix"></div><div class="box clearfix"></div>`
                $(`#${this.id}`).html(str)  ;
            }
            initTop(){ // 初始化tab栏
                var ul = document.createElement("ul")
                var str="";
                for(var i = 0 ; i< this.arrs.length ; i ++){
                    str +=`<li class="${i == 0 ? "on" : ""}" οnclick="${this.toggle(this.id , this)}">${this.arrs[i].tabname}</li>`
                }
                ul.innerHTML = str
                $(`#${this.id} .tab`).html(ul);
            }
            initBot(){
                var ul = document.createElement("ul")
                var str="";
                for(var i = 0 ; i< this.arrs.length ; i ++){
                    str +=`<li class="${i == 0 ? "on" : ""}">${this.arrs[i].info}</li>`
                }
                ul.innerHTML = str
                $(`#${this.id} .box`).html(ul);
            }
            toggle(id , that){
                
                $(`#${id} .tab`).on("click","ul li",function(){
                    $(this).addClass("on").siblings().removeClass("on");
                    $(`#${that.id} .box ul li`).removeClass("on").eq($(this).index()).addClass("on");
                })
            }
        }
        var arrs = [
            {
                tabname:"张三",
                info:"张三位于郑州"
            },
            {
                tabname:"李四",
                info:"李四位于上海"
            },
            {
                tabname:"王五",
                info:"王五位于北京"
            },
        ]
        var tabs = new Tab("main" , arrs)
        var tabs1 = new Tab("main1" , arrs)
    </script>
</body>
</html>
</script>

Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,由此我们可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

语法:

let proxy = new Proxy(target, handler)

  • target —— 是要包装的对象,可以是任何东西,包括函数。
  • handler —— 代理配置:声明了代理 target 的哪些操作
  • proxy 是被代理完成之后返回的对象

proxy 进行操作,如果在 handler 中存在相应的设置,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。

对于每个内部方法,此表中都有一个设置:可用于添加到 new Proxyhandler 参数中以拦截操作的方法名称:

内部方法 Handler 方法 何时触发
[[Get]] get 读取属性
[[Set]] set 写入属性
[[HasProperty]] has in 操作符
[[Delete]] deleteProperty delete 操作符
[[Call]] apply 函数调用
[[Construct]] construct new 操作符
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object/keys/values/entries

注意:要使得 Proxy 起作用,必须针对 Proxy 实例进行操作,而不是针对目标对象进行操作。

如果 handler 没有设置任何拦截,那就等同于直接通向原对象。

最常见的设置是用于读取/写入的属性。

get

要拦截读取操作,handler 应该有 get(target, property, receiver) 方法。

读取属性时触发该方法,参数如下:

  • target — 是目标对象,该对象被作为第一个参数传递给 new Proxy
  • property — 目标属性名,
  • receiver — 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。通常,这就是 proxy 对象本身。可选。
// 如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined。
var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name " + propKey + " does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误

set

当写入属性时 set 设置被触发。

set(target, property, value, receiver)

  • target — 是目标对象,该对象被作为第一个参数传递给 new Proxy
  • property — 目标属性名称,
  • value — 目标属性的值,
  • receiver — 与 get 设置类似,仅与 setter 访问器属性相关。

如果写入操作(setting)成功,set 设置应该返回 true,否则返回 false(触发 TypeError)。

var person = {
    name: "张三",
    age:20
};

var proxy = new Proxy(person, {
    set(target, key, val){
        if(key == "age"){
            if(val>150){
                throw new Error("年龄不符合要求")
            }else{
                target[key] = val
            }
        }
    }
});
proxy.age = 200;

has

has 方法会拦截 in 调用。

has(target, property)

  • target — 是目标对象,被作为第一个参数传递给 new Proxy
  • property — 属性名称

示例如下:

// 创建一个范围对象
let range = {
  start: 1,
  end: 10
};

// 设置代理,监听in操作符 -> 更换它的逻辑
range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end;
  }
});

alert(5 in range); // true
alert(50 in range); // false

apply

我们也可以将代理(proxy)包装在函数周围。

apply(target, thisArg, args) 方法能使代理以函数的方式被调用:

  • target 是目标对象(在 JavaScript 中,函数就是一个对象),
  • thisArgthis 的值。
  • args 是参数列表。

例如:

// 之前的延时包装器函数
function delay(f, ms){
    return function(...args){
        setTimeout(function(){
            f.apply(null, args);
        }, ms)
    }
}

function say(word){
    console.log("Hi " + word);
}

let f1000 = delay(say, 1000);

// 包装函数不会转发属性读取/写入操作或者任何其他操作。进行包装后,就失去了对原始函数属性的访问,例如 `name`,`length` 和其他属性
console.log(f1000.length); // 0 

Proxy 的功能要强大得多,因为它可以将所有东西转发到目标对象。

让我们使用 Proxy 来替换掉包装函数:

function delay(f, ms){
    return new Proxy(f, {
        apply(target, thisArg, args){
            setTimeout(() => target(...args), ms);
        }
    })
}

function say(word){
    console.log("Hi " + word);
}

let f2000 = delay(say, 2000);
console.log(f2000.length); // 1

受保护的属性

有一个普遍的约定,即以下划线 _ 开头的属性和方法是内部的。不应从对象外部访问它们,但实际上我们也是能访问到这样的属性的:

let user = {
    name: "李白",
    _age: 23
}
console.log(user._age); // 23

我们可以使用代理来防止对以 _ 开头的属性的任何访问。

我们将需要以下内容:

  • get 读取此类属性时抛出错误
  • set 写入属性时抛出错误
  • deleteProperty 删除属性时抛出错误
  • ownKeys 在使用 for..in 和像 Object.keys 这样的的方法时排除以 _ 开头的属性
let user = {
    name: "李白",
    _age: 23
}

user = new Proxy(user, {
    get(target, prop){
        if(prop[0] == '_'){
            throw new Error("不能访问私有属性");
        }
        return target[prop];
    },
    set(target, prop, val){
        if(prop[0] == '_'){
            throw new Error("不能访问私有属性");
        }
        target[prop] = val;
    },
    deleteProperty(target, prop){
        if(prop[0] == '_'){
            throw new Error("不能访问私有属性");
        }
        delete target[prop];
    },
    ownKeys(target){
        return Object.keys(target).filter(item => !(item[0] == "_"));
    }
})

// "get" 不允许读取 _age
try{
    console.log(user._age);
}catch(e){
    console.log("get Error", e);
}
// "set" 不允许写入 _age
try{
   user._age = 20;
}catch(e){
    console.log("set Error", e);
}
// "deleteProperty" 不允许删除 _age
try{
    delete user._age;
}catch(e){
    console.log("delete Error", e);
}
// "ownKeys" 将 _age 过滤出去
for(let key in user){
    console.log("遍历", key);
}

练习

1、在某些编程语言中,我们可以使用从尾端算起的负值索引访问数组元素。即 array[-N]array[array.length - N] 相同。

像这样:

let array = [1, 2, 3];

array[-1]; // 3,最后一个元素
array[-2]; // 2,从尾端开始向前移动一步
array[-3]; // 1,从尾端开始向前移动两步

创建一个 proxy 来实现该行为。

Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

基础语法及理解

let promise = new Promise(function(resolve, reject){
    // do somthing async...
})

传递给 new Promise 的函数被称为 executor(执行者)。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。

它的参数 resolvereject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部。

当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:

  • resolve(value) — 如果任务成功完成并带有结果 value
  • reject(error) — 如果出现了 error,error 即为 error 对象。

所以总结一下就是:executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject

new Promise 构造器返回的 promise 对象具有以下内部属性:

  • state — 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"
  • result — 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error
let p1 = new Promise(resolve => {
    setTimeout(function(){
        resolve("success");
        console.log(p1);
    }, 2000);
})

console.log(p1);

let p2 = new Promise((resolve, reject) => {
    setTimeout(function(){
        reject("success");
        console.log(p2);
    }, 2000);
})

console.log(p2);

注意:

  1. executor 只能调用一个 resolve 或一个 reject。任何状态的更改都是最终的。

    所有其他的再对 resolvereject 的调用都会被忽略:

    let p3 = new Promise((resolve, reject) => {
        resolve("done");
    
        reject(new Error("…")); // 被忽略
        setTimeout(() => resolve("…"), 0); // 被忽略
    });
    
    
  2. resolve/reject 只需要一个参数(或不包含任何参数),并且将忽略额外的参数。

then

语法:

promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

.then 的第一个参数是一个函数,该函数将在 promise resolved 后运行并接收结果。

.then 的第二个参数也是一个函数,该函数将在 promise rejected 后运行并接收 error。

例如,以下是对成功 resolved 的 promise 做出的反应:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("成功!"), 1000);
});

// resolve 运行 .then 中的第一个函数
promise.then(
  result => alert(result), // 1 秒后显示 "成功!"
  error => alert(error) // 不运行
);

在 reject 的情况下,运行第二个:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("报错了!")), 1000);
});

// reject 运行 .then 中的第二个函数
promise.then(
  result => alert(result), // 不运行
  error => alert(error) // 1 秒后显示 "Error: 报错了!"
);

当前,我们也可以只传入成功的函数

catch

如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("报错!")), 1000);
});

// .catch(func) 与 promise.then(null, func) 一样
promise.catch(alert); // 1 秒后显示 "Error: 报错!"

.catch 能够处理 promise 中的各种 error:在 reject() 调用中的,或者在处理程序中抛出的(thrown)error(必须是同步错误)。

let promise = new Promise((resolve, reject) => {
  throw new Error("报错!")
});

// .catch(func) 与 promise.then(null, func) 一样
promise.catch(alert); // 1 秒后显示 "Error: 报错!"

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => { throw new Error("报错!"); }, 1000);
});

// .catch(func) 与 promise.then(null, func) 一样
promise1.catch(alert); // 无法捕获

finally

  1. finally 处理函数没有参数。在 finally 中,我们不知道 promise 是否成功。一般来说此方法用于执行“常规”的定稿程序(finalizing procedures)。
  2. finally 处理函数并不处理 promise 的结果,而是将结果传递给下一个处理程序。

使用1:

new Promise((resolve, reject) => {
  setTimeout(() => resolve("result"), 2000)
})
  .finally(() => alert("Promise ready"))
  .then(result => alert(result)); // <-- .then 对结果进行处理

使用2:

new Promise((resolve, reject) => {
  throw new Error("error");
})
  .finally(() => alert("Promise ready"))
  .catch(err => alert(err));  // <-- .catch 对 error 对象进行处理

Promise 链

实际上 promise.then 方法返回值也是一个 promise 对象,它可以形成一个链式调用。

then 函数内部的处理函数的返回值会成为下一个 promise 对象的 result,所以将使用它调用下一个 .then

调用 promise.then 方法返回的 promise 对象的状态由 then 方法的回调函数执行结果决定:

  1. 如果回调函数中返回的结果时 非promise 类型的 属性,状态成功,返回值为对象的成功值
  2. 是 promise 对象,根据对象的返回状态决定
  3. 抛出错误,状态失败,接受错误内容

例如:

let promise = new Promise(function(resolve, reject){
    setTimeout(() => resolve(1), 2000);
}).then(res => {
    console.log("第一次执行成功回调", res * 2);
    return res * 2;
}).then(res => {
    console.log("第二次执行成功回调", res * 2);
    return res * 2;
}).then(res => {
    console.log("第三次执行成功回调", res * 2);
})

又或者:

let permise = new Promise(resolve => {
    setTimeout(function () {
        resolve("success");
    }, 2000);
}).then(function (res){
    console.log("第一个 then 方法", res, "函数内抛出异常");
    throw new Error("错误 1998");
}).catch(function (err){
    console.log("捕获异常:", err, "函数内返回字符串");
    return "success";
}).then(function (res){
    console.log("在异常后面继续接受成功的内容",res, "返回一个promise");
    return new Promise(resolve => {
        setTimeout(function (){
            resolve("last")
        }, 1000)
    })
}).then(function (res){
    console.log("最后一站 接收到上一个promise成功的返回值", res);
})

Promise静态方法

Promise.all

假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪,我们可以使用此方法来实现。

语法:

let promise = Promise.all([...promises...]);

Promise.all 接受一个 promise 数组作为参数(从技术上讲,它可以是任何可迭代的,但通常是一个数组)并返回一个新的 promise。

当所有给定的 promise 都被 settled 时,新的 promise 才会 resolve,并且其结果数组将成为新的 promise 的结果。

例如:

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log); 

请注意,结果数组中元素的顺序与其在源 promise 中的顺序相同。即使第一个 promise 花费了最长的时间才 resolve,但它仍是结果数组中的第一个。

注意:如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("错误!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(console.log); // Error: 错误!

Promise.allSettled

假如我们组合使用 promise 做请求,即使有一部分失败我们仍想获取其它的数据,此方法很有用。Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组具有:

  • {status:"fulfilled", value:result} 对于成功的响应,
  • {status:"rejected", reason:error} 对于 error。

例如:

Promise.allSettled([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("错误!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.log);

结果为:

[
    {status: "fulfilled", value: 1},
    {status: "rejected", reason: Error: 错误! at <anonymous>:3:60},
    {status: "fulfilled", value: 3}
]

Promise.race

Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。

语法:

let promise = Promise.race(iterable);

例如:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.log); // 1

这里第一个 promise 最快,所以它变成了结果。第一个 settled 的 promise “赢得了比赛”之后,所有进一步的 result/error 都会被忽略。

Async/await

Async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

Async function

让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:

async function f(){
    return 1;
}

在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise,函数的返回值决定 promise 对象的完成状态 参考

f().then(console.log); // 1

我们也可以显式地返回一个 promise,结果是一样的:

async function f() {
  return new Promise(resolve => resolve(10));
}

f().then(console.log); // 10

Await

语法如下:

// 只在 async 函数内工作
let value = await promise;

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。

这里的例子就是一个 2 秒后 resolve 的 promise:

async function f(){
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("done!"), 2000);
    })
    
    let result = await promise; // 等待,直到 promise resolve (*)
    
    console.log(result); // done!
}

f();

这个函数在执行的时候,“暂停”在了 (*) 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”。

总结:

  1. 函数前面的关键字 async 有两个作用:
    1. 让这个函数总是返回一个 promise
    2. 允许在该函数内使用 await
  2. Promise 前的关键字 await 使 JavaScript 引擎等待该 promise settle,然后:
    1. 如果有 error,就会抛出异常——就像那里调用了 throw error 一样
    2. 否则,就返回结果