JS之详细理解异步编程上——前端之JavaScript高级之六【Day57-Day63】

216 阅读27分钟

挑战坚持学习1024天——前端之JavaScript高级

js基础部分可到我文章专栏去看 ---点击这里

Day57【2022年9月19日】

学习重点:异步编程之初识primose(期约)

1.1primose背景

为弥合现有实现之间的差异,2012 年 Promises/A+组织分叉(fork)了 CommonJS 的 Promises/A 建议,并以相同的名字制定了 Promises/A+规范。这个规范最终成为了ECMAScript 6 规范实现的范本。ECMAScript 6 增加了对 Promises/A+规范的完善支持,即 Promise 类型。一经推出,Promise 就大受欢迎,成为了主导性的异步编程机制。所有现代浏览器都支持 ES6 期约,很多其他浏览器 API(如fetch()和 Battery Status API)也以期约为基础。

1.2promise简单运用

Promise是一个类,可以翻译成 承诺、许诺 、期约;

当我们需要的时候,给予调用者一个承诺:待会儿我会给你回调数据时,就可以创建一个Promise的对象;

在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor(执行器)

  • 这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject;
  • 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数;
  • 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数;

可以通过 new 操作符来实例化。创建新期约时需要传入执行器(executor)函数作为参数 在 Promise 实例创建后,执行器里的逻辑会立刻执行,在执行的过程中,根据异步返回的结果,决定如何使用 resolve 或 reject 来改变 Promise实例的状态。 Promise 实例有三种状态:

• pending 状态,表示进行中。这是 Promise 实例创建后的一个初始态; • fulfilled 状态,表示成功完成。这是我们在执行器中调用 resolve 后,达成的状态; • rejected 状态,表示操作失败、被拒绝。这是我们在执行器中调用 reject后,达成的状态。 Promise实例的状态是可以改变的,但它只允许被改变一次。当我们的实例状态从 pending 切换为 rejected 后,就无法再扭转为 fulfilled,反之同理。当 Promise 的状态为 resolved 时,会触发其对应的 then 方法入参里的 onfulfilled 函数;当 Promise 的状态为 rejected 时,会触发其对应的 then 方法入参里的 onrejected 函数。(promise第一次状态被改变后将无法改变)

实例

console.log("Start");
//promise初始状态为pending 改变为其他状态后将不会再改变
let p = new Promise(function(resolve, reject){
   //这里会同步执行
   console.log("Promise");
   //resolve/reject是异步代码
   //调用resolve()时,后续的then()方法被调用
   resolve('success');
   //调用reject()时,后续的catch()方法被调用
   console.log("End");
   reject('error'); //不会被执行
});
p.then(function(msg){
   console.log(msg);
}).catch(function(err){
   console.log(err);
});

//reject()方法的参数会原封不动地作为catch()方法的参数
let p1 = new Promise(function(resolve, reject){
   reject('error');
}
);
p1.catch(function(err){
   console.log(err);
});

// Start
// Promise
// End
// success
// error

promise resolve遇上console.log,reject也是如此

//下面两个代码是等价的
let a = new Promise((resolve, reject) => {
    resolve(console.log(1));  //相当先执行console.log(1) 后执行resolve()
});
console.log(2);
console.log(a);


let a1 = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
});
console.log(2);
console.log(a1);

解决了回调地狱

2.今日精进

做事按照既定流程,迈出第一步时已经成功一半了。

Day58-Day59【2022年9月20日-21日】

学习重点:异步编程之primose详解

1.1 执行器(Executor)

Executor是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数(resolve, reject) 通常我们会在Executor中确定我们的Promise状态:

通过resolve,可以兑现(fulfilled)Promise的状态,我们也可以称之为已决议(resolved);

通过reject,可以拒绝(reject)Promise的状态;

实例:

new Promise((resolve, reject) => {
   console.log("executor");
})

1.2 Promise 方法

1.2.1 resolve

1.2.1.1 基本用法

期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用 Promise.resolve()静态方法,可以实例化一个解决的期约。下面两个期约实例实际上是一样的:

let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();

语法

Promise.resolve(value);

接收一个参数value表示将被 Promise 对象解析的参数也可以是一个Promise 对象,或者是一个 thenable。(三种)

then()作为promise的配套组件,我们只需要验证有没有then()就可以了,所以,我们提出了thenable的概念。thenable:任何含有then()方法的对象或函数。它的结构是不是很像promise,它们都有一个then()。所以,我们无法找出promise时,就退而求其次,先找类似promise的,也就是thenable。 thenable文章[详解](blog.csdn.net/weixin_4230…)

返回值

返回一个带着给定值解析过的 Promise 对象,如果参数本身就是一个 Promise 对象,则直接返回这个 Promise 对象。 详情可看 实例

//resolve
let p2 = new Promise(function(resolve, reject){
   resolve('success');
});
p2.then(function(msg){
   console.log(msg);
});

let p3 = Promise.resolve('success');
p3.then(function(msg){
   console.log(msg);
});

Promise.resolve('success').then(function(msg){
   console.log(msg);
});
//success
//success
//success

以上三种写法等价

对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此, Promise.resolve()可以说是一个幂等方法,如下所示:

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true

这个幂等性会保留传入期约的状态:

let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); // true

1.2.1.2resolve不同值的区别

  • 情况一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数;
//resovle 传入普通值或对象
new Promise((resolve, reject) => {
   resolve('normal resolve');
}).then((msg) => {
   console.log(msg);
});
//normal resolve
  • 情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态:
//resolve 传入promise对象
new Promise((resolve, reject) => {
   resolve(new Promise((resolve, reject) => {
      setTimeout(()=>{
         resolve('promise resolve');
      },1000);
      }));
}).then((msg) => {
   console.log(msg);
}).catch((err) => {
   console.log(err);
});
  • 情况三:如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据then方法的结果来决定Promise的状态:
//resolve 传入有then方法的对象
//状态由then方法决定
new Promise((resolve, reject) => {
   resolve({
      then: function(resolve, reject){
         // resolve('object resolve');
         reject('object reject');
      }
   });
}).then((msg) => {
   console.log(msg);
}).catch((err)=>{
   console.log(err);
})

1.2.2 then

1.2.2.1基本用法

then方法是Promise对象上的一个方法(实例方法)它其实是放在Promise的原型上的 Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。一般情况下使用只用成功的状态。对应resolve。

语法

p.then(onFulfilled[, onRejected]);

p.then(value => {
  // fulfillment
}, reason => {
  // rejection
});

then方法接受两个参数:

fulfilled的回调函数:当状态变成fulfilled时会回调的函数;

reject的回调函数:当状态变成reject时会回调的函数;

两个参数都是可选

返回值

then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。 由下图可看到promise的返回值 image.png

当一个 Promise  完成(fulfilled)或者失败(rejected)时,返回函数将被异步调用(由当前的线程循环来调度完成)。具体的返回值依据以下规则返回。如果 then 中的回调函数:

  • 返回了一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
  • 没有返回任何值,那么 then 返回的 Promise 将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined
  • 抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 返回一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的 Promise 的接受状态回调函数的参数值。
  • 返回一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的 Promise 的拒绝状态回调函数的参数值。
  • 返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。

总结如下

当then方法中的回调函数本身在执行的时候,那么它处于pending状态;

当then方法中的回调函数返回一个结果时,那么它处于fulfilled状态,并且会将结果作为resolve的参数;

情况一:返回一个普通的值;

情况二:返回一个Promise;

情况三:返回一个thenable值;

当then方法抛出一个异常时,那么它处于reject状态;

实例

//then 
//onFulfilled/onRejected是可选参数
//如果不是函数,会被忽略
//如果onFulfilled/onRejected返回一个值,会被传递给下一个then的onFulfilled
//如果onFulfilled/onRejected抛出一个异常,会被传递给下一个then的onRejected
//如果onFulfilled/onRejected返回一个promise对象,会等待这个promise对象的状态改变,再传递给下一个then的onFulfilled/onRejected

let promise = new Promise((resolve, reject) => {
   resolve('success');
   // reject('error');
});
promise.then((res)=>{
   console.log(res);
}, (err)=>{
   console.log(err);
});
//等价于
promise.then((res)=>{
   console.log(res);
}).catch((err)=>{
   console.log(err);
});

1.2.2.2 链式调用

一个Promise的then方法是可以被多次调用的:每次调用我们都可以传入对应的fulfilled回调;当Promise的状态变成fulfilled的时候,这些回调函数都会被执行;

实例

//then链式调用
// 用法1
let promise = new Promise((resolve, reject) => {
   resolve('success');
});
promise.then((res)=>{
   console.log(res);
   return 'success1';
}).then((res)=>{
   console.log(res);
   return 'success2';
}).then((res)=>{
   console.log(res);
});
//success
//success1
//success2


//用法2
let promise1 = new Promise((resolve, reject) => {
   resolve('success');
});
promise1.then((res)=>{
   console.log(res);
}).then((res)=>{
   console.log(res);
}).then((res)=>{
   console.log(res);
}
);

//success
//undefined
//undefined

//用法3
let promise2 = new Promise((resolve, reject) => {
   resolve('success');
});
promise2.then((res)=>{
   console.log(res);
   return new Promise((resolve, reject) => {
      resolve('success1');
   });
}).then((res)=>{
   console.log(res);
   return new Promise((resolve, reject) => {
      resolve('success2');
   });
}).then((res)=>{
   console.log(res);
});

//success
//success1
//success2

//用法4
let promise3 = new Promise((resolve, reject) => {
   resolve('success');
});
promise3.then((res)=>{
   console.log(res);
   return new Promise((resolve, reject) => {
      reject('error');
   });
}).then((res)=>{
   console.log(res);
   return new Promise((resolve, reject) => {
      resolve('success2');
   });
}).then((res)=>{
   console.log(res);
}).catch((err)=>{
   console.log(err);
});

//success
//error

//用法5
let promise4 = new Promise((resolve, reject) => {
   resolve('success');
});

promise4.then((res)=>{
   console.log("res1:",res)
});

promise4.then((res)=>{
   console.log("res2:",res)
});

// res1: success
// res2: success

    Promise.resolve("foo")
    // 1. 接收 "foo" 并与 "bar" 拼接,并将其结果做为下一个 resolve 返回。
    .then(function(string) {
      return new Promise(function(resolve, reject) {
        setTimeout(function() {
          string += 'bar';
          resolve(string);
        }, 1);
      });
    })
    // 2. 接收 "foobar", 放入一个异步函数中处理该字符串
    // 并将其打印到控制台中,但是不将处理后的字符串返回到下一个。
    .then(function(string) {
      setTimeout(function() {
        string += 'baz';
        console.log(string);
      }, 1)
      return string;
    })
    // 3. 打印本节中代码将如何运行的帮助消息,
    // 字符串实际上是由上一个回调函数之前的那块异步代码处理的。
    .then(function(string) {
      console.log("Last Then:  oops... didn't bother to instantiate and return " +
                  "a promise in the prior then so the sequence may be a bit " +
                  "surprising");
  
      // 注意 `string` 这时不会存在 'baz'。
      // 因为这是发生在我们通过 setTimeout 模拟的异步函数中。
      console.log(string);
    });
  

由上可见,then值接收函数,如果已经被一个then处理了后面的then则接收不到resolve的值,如果想要接收到可通过resolve/return的方式去接收。

1.2.3 reject

reject方法类似于resolve方法,只是会将Promise对象的状态设置为reject状态。

Promise.reject的用法相当于new Promise,只是会调用reject

Promise.reject传入的参数无论是什么形态,都会直接作为reject

语法

Promise.reject(reason);

reason接收的拒绝的原因

返回值

一个给定原因了的被拒绝的 Promise。

实例

以下两种写法等价
let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();

以下三种写法等价
let p1 = new Promise((resolve, reject) => {
   reject("why");
});
p1.catch((err) => {
   console.log(err);
});

let p2 = Promise.reject("why");
p2.catch((err) => {
   console.log(err);
});

Promise.reject("why").catch((err) => {
   console.log(err);
});

关键在于,Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期约对象,则这个期约会成为它返回的拒绝期约的理由:

setTimeout(console.log, 0, Promise.reject(Promise.resolve()))
// Promise <rejected>: Promise <resolved>

1.2.3.1 resolve和reject幂等性验证

resolve是幂等性 reject非幂等

//返回值是一个promise对象 类似一个空包装 幂等
let p1 = Promise.resolve("success");
console.log(p1);//Promise {<fulfilled>: 'success'}
console.log(Promise.resolve(p1)); //Promise {<fulfilled>: 'success'}
console.log(Promise.resolve(p1) === p1); //true

//返回值是一个带理由的promise对象 非幂等
let p2 = Promise.reject("error");
console.log(p2);//Promise {<rejected>: 'error'}
console.log(Promise.reject(p2)); //PPromise {<rejected>: Promise}
console.log(Promise.reject(p2) === p2); //false

1.2.4 catch

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)。

语法

p.catch(onRejected);

p.catch(function(reason) {
   // 拒绝
});

onRejected 当 Promise 被 rejected 时,被调用的一个Function。 该函数拥有一个参数:reason rejection 的原因。如果 onRejected 抛出一个错误或返回一个本身失败的 Promise , 通过 catch() 返回的 Promise 被 rejected;否则,它将显示为成功(resolved)。 返回值

一个新的promise实例与原来不相等

let p1 = new Promise(() => {}); 
let p2 = p1.catch(); 
setTimeout(console.log, 0, p1); // Promise <pending> 
setTimeout(console.log, 0, p2); // Promise <pending> 
setTimeout(console.log, 0, p1 === p2); // false

实例

Promise.prototype.catch(onRejected) 等价于Promise.prototype.then(undefined, onRejected)

let p1 =  Promise.reject("error");
//以下两种写法等价
p1.catch((err) => {
   console.log(err);
});
p1.then(undefined,(err)=>{
   console.log(err);
})

//也可以用这种写法
// let p = Promise.reject(); 
// let onRejected = function(e) { 
//  setTimeout(console.log, 0, 'rejected'); 
// }; 
// // 这两种添加拒绝处理程序的方式是一样的:
// p.then(null, onRejected); // rejected 
// p.catch(onRejected); // rejected

1.2.5 all

Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set 都属于 ES6 的 iterable 类型)的输入,并且只返回一个Promise实例, 那个输入的所有 promise 的 resolve 回调的结果是一个数组。这个Promise的 resolve 回调执行是在所有输入的 promise 的 resolve 回调都结束,或者输入的 iterable 里没有 promise 了的时候。它的 reject 回调执行是,只要任何一个输入的 promise 的 reject 回调执行或者输入不合法的 promise 就会立即抛出错误,并且 reject 的是第一个抛出的错误信息。

它的作用是将多个Promise包裹在一起形成一个新的Promise;新的Promise状态由包裹的所有Promise共同决定:当所有的Promise状态变成resolved状态时,新的Promise状态为resolved,并且会将所有Promise的返回值组成一个数组;当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数。

语法

Promise.all(iterable);

iterable一个可迭代对象,如 Array 或 String。

返回值

  • 如果传入的参数是一个空的可迭代对象,则返回一个已完成(already resolved)状态的 Promise。
  • 如果传入的参数不包含任何 promise,则返回一个异步完成(asynchronously resolved)Promise。注意:Google Chrome 58 在这种情况下返回一个已完成(already resolved)状态的 Promise。
  • 其它情况下返回一个处理中(pending)的Promise。这个返回的 promise 之后会在所有的 promise 都完成或有一个 promise 失败时异步地变为完成或失败。 见下方关于“Promise.all 的异步或同步”示例。返回值将会按照参数内的 promise 顺序排列,而不是由调用 promise 的完成顺序决定。

完成(Fulfillment): 如果传入的可迭代对象为空,Promise.all 会同步地返回一个已完成(resolved)状态的promise。 如果所有传入的 promise 都变为完成状态,或者传入的可迭代对象内没有 promise,Promise.all 返回的 promise 异步地变为完成。 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组,它包含所有的传入迭代参数对象的值(也包括非 promise 值)。

失败/拒绝(Rejection): 如果传入的 promise 中有一个失败(rejected),Promise.all 异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成。

实例 调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。(接收后)

let p1 = new Promise((resolve, reject) => {
   resolve("success1");
});
let p2 = new Promise((resolve, reject) => {
   resolve("success2");
});
let p3 = new Promise((resolve, reject) => {
   resolve("success3");
});

Promise.all([p1, p2, p3]).then((res) => {
   console.log(res);
}).catch((err) => {
   console.log(err);
});

//[ 'success1', 'success2', 'success3' ]

如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的 期约也会拒绝:

// 永远待定
let p1 = Promise.all([new Promise(() => {})]); 
setTimeout(console.log, 0, p1); // Promise <pending> 
// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([ 
 Promise.resolve(), 
 Promise.reject(), 
 Promise.resolve() 
]); 
setTimeout(console.log, 0, p2); // Promise <rejected> 
// Uncaught (in promise) undefined

1.2.5.1 allSettled

all方法有一个缺陷: 当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的;在ES11(ES2020)中,添加了新的API Promise.allSettled:

// new Promise(() => {}) 表达待定状态的promise对象
//resolve状态
let p1 = new Promise((resolve, reject) => {
   resolve("success1");
});
let p2 = new Promise((resolve, reject) => {
   resolve("success2");
});
let p3 = new Promise((resolve, reject) => {
   resolve("success3");
});
//padding状态
let p4 = new Promise(() => {});
//reject状态
let p5 = new Promise((resolve, reject) => {
   reject("error");
});

Promise.allSettled([p1, p2, p3,p5]).then((res) => {
   console.log(res);
}).catch((err) => {
   console.log(err);
});

// (4) [{…}, {…}, {…}, {…}]
// 0
// : 
// {status: 'fulfilled', value: 'success1'}
// 1
// : 
// {status: 'fulfilled', value: 'success2'}
// 2
// : 
// {status: 'fulfilled', value: 'success3'}
// 3
// : 
// {status: 'rejected', reason: 'error'}
// length
// : 
// 4
// [[Prototype]]
// : 
// Array(0)

该方法会在所有的Promise都有结果(settled),无论是fulfilled,还是rejected时,才会有最终的状态;并且这个Promise的结果一定是fulfilled的;但是如果存在padding状态整个promise还是返回待定状态

//resolve状态
let p1 = new Promise((resolve, reject) => {
   resolve("success1");
});
let p2 = new Promise((resolve, reject) => {
   resolve("success2");
});
let p3 = new Promise((resolve, reject) => {
   resolve("success3");
});
//padding状态
let p4 = new Promise(() => {});
//reject状态
let p5 = new Promise((resolve, reject) => {
   reject("error");
});

Promise.allSettled([p1, p2, p3,p4,p5]).then((res) => {
   console.log(res);
}).catch((err) => {
   console.log(err);
});

Promise {<pending>}

我们来看一下打印的结果: allSettled的结果是一个数组,数组中存放着每一个Promise的结果,并且是对应一个对象的;这个对象中包含status状态,以及对应的value值; 当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。相比之下,Promise.all() 更适合彼此相互依赖或者在其中任何一个reject时立即结束。

1.2.6 race

如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race方法: race是竞技、竞赛的意思,表示多个Promise相互竞争,谁先有结果,那么就使用谁的结果; race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected。

语法

Promise.race(iterable);

iterable可迭代对象,类似Array。详见 iterable。

返回值

一个待定的 Promise 只要给定的迭代中的一个 promise 解决或拒绝,就采用第一个 promise 的值作为它的值,从而异步地解析或拒绝(一旦堆栈为空)。

实例

let p1 = new Promise((resolve,reject)=>{
   setTimeout(()=>{
      reject(1);
   },2000)
});
let p2 = new Promise((resolve,reject)=>{
   setTimeout(()=>{
      resolve(2);
      //即使是返回失败的状态也会执行输出2
      //reject(2);
   },1000)
});
let p3 = new Promise((resolve,reject)=>{
   setTimeout(()=>{
      resolve(3);
   },3000)
});

Promise.race([p1,p2,p3]).then((res)=>{
   console.log(res)
}).catch((res)=>{
   console.log(res)
});
//2

all、race、reject 和 resolve。为重点方法

1.2.7 any

该方法还属于实验性方法,并不适用于所有的浏览器 any方法是ES12中新增的方法,和race方法是类似的: any方法会等到一个fulfilled状态,才会决定新Promise的状态; 如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态; 如果所有的Promise都是reject的,那么会报一个AggregateError的错误。

语法

Promise.any(iterable);

iterable 一个可迭代的对象,例如 Array。

返回值

如果传入了一个空的可迭代对象,那么就会返回一个已经被拒的 promise 如果传入了一个不含有 promise 的可迭代对象,那么就会返回一个异步兑现的 promise 其余情况下都会返回一个处于等待状态的 promise。如果可迭代对象中的任意一个 promise 兑现了,那么这个处于等待状态的 promise 就会异步地(调用栈为空时)切换至兑现状态。如果可迭代对象中的所有 promise 都被拒绝了,那么这个处于等待状态的 promise 就会异步地切换至被拒状态。

1.2.8 finally

finally是在ES9(ES2018)中新增的一个特性:表示无论Promise对象无论变成fulfilled还是rejected状态,最终都会被执行的代码。finally方法是不接收参数的,因为无论前面是fulfilled状态,还是rejected状态,它都会执行。

语法

p.finally(onFinally);

p.finally(function() {
   // 返回状态为 (resolved 或 rejected)
});

onFinally Promise 结束后调用的 Function。

返回值 返回一个设置了 finally 回调函数的 Promise 对象。

实例

let p1 = new Promise((resolve,reject)=>{
    // resolve("success");
    reject("false");
}).then((res)=>{
    console.log(res);
}).catch((err)=>{
    console.log(err);
}).finally(()=>{
    console.log("finally");
});

无论结果如何都会执行finally方法

如果你想在 promise 执行完毕后无论其结果怎样都做一些处理或清理时,finally() 方法可能是有用的。

finally() 虽然与 .then(onFinally, onFinally) 类似,它们不同的是:

调用内联函数时,不需要多次声明该函数或为该函数创建一个变量保存它。 由于无法知道 promise 的最终状态,所以 finally 的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。 与Promise.resolve(2).then(() => {}, () => {})(resolved 的结果为undefined)不同,Promise.resolve(2).finally(() => {}) resolved 的结果为 2。 同样,Promise.reject(3).then(() => {}, () => {}) (fulfilled 的结果为undefined), Promise.reject(3).finally(() => {}) rejected 的结果为 3。

Promise.resolve(2).then(() => {}, () => {});
//Promise {<fulfilled>: undefined} 返回值(浏览器)

Promise.resolve(2).finally(() => {});
//Promise {<fulfilled>: 2}

实例方法(挂载在原型链上) :Promise.prototype.then()、Promise.prototype.catch()、Promise.prototype.finally()、 期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码。

2.今日精进

要控制自身情绪,保持乐观,宁愿错误的乐观,也不行正确的悲观。

Day60-61【2022年9月22日-23日】

学习重点:迭代器(Iterator)

1.什么是迭代器

1.1迭代

理解一下迭代 循环是最简单的迭代 实例

for (let i = 1; i <= 10; ++i) {
    console.log(i);
} 

循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。迭代会在一个有序集合上进行。(“有序”可以理解为集合中所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义。)数组是 JavaScript 中有序集合的最典型例子。

let collection = ['foo', 'bar', 'baz']; 
for (let index = 0; index < collection.length; ++index) { 
 console.log(collection[index]); 
} 

但是这种方式也有一定的弊端,迭代在执行操作时需要事情知道如何迭代。

迭代弊端

迭代之前需要事先知道如何使用数据结构。 数组中的每一项都只能先通过引用取得数组对象, 然后再通过[]操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构。 遍历顺序并不是数据结构固有的。 通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构。

虽然后续推出了forEach也有一定弊端(只能遍历数组,无法终止循环而且回调结构也比较笨拙)。

迭代器推出的原因

ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。Python、Java、C++,还有其他很多语言都对这个模式提供了完备的支持。JavaScript 在 ECMAScript 6 以后也支持了迭代器模式。

1.2迭代器(Iterator)

在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 更具体地说,迭代器是通过使用 next() 方法实现 Iterator protocol(迭代协议) 的任何一个对象, 该方法返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。 迭代协议请看-->这

迭代器(iterator),使用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。 其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;

迭代器是帮助我们对某个数据结构进行遍历的对象。

在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):

迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;

在JavaScript中这个标准就是一个特定的next方法;

next方法有如下的要求:

一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:

done(boolean)

  • 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)

  • 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。

value

  • 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

实现迭代器

const friends = ['Michael', 'Stacy', 'Andy'];

function createIterator(items) {
    let i = 0;
    return {
        next: function() {
            let done = (i >= items.length);
            let value = !done ? items[i++] : undefined;
            return {
                done: done,
                value: value
            };
        }
    };
}

const iterator = createIterator(friends);
console.log(iterator.next()); // { done: false, value: 'Michael' }
console.log(iterator.next()); // { done: false, value: 'Stacy' }
console.log(iterator.next()); // { done: false, value: 'Andy' }
console.log(iterator.next()); // { done: true, value: undefined }

1.3迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next()方法 在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭 代器返回的下一个值。若不调用 next(),则无法知道迭代器的当前位置。 next()方法返回的迭代器对象 IteratorResult 包含两个属性:done 和 value。done 是一个布 尔值,表示是否还可以再次调用 next()取得下一个值;value 包含可迭代对象的下一个值(done 为 false),或者 undefined(done 为 true)。done: true 状态称为“耗尽”。

有以下几条规则:

  • 不同迭代器直接没有联系
  • 如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
  • 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。
// 可迭代对象
let arr = ['foo', 'bar']; 
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] } 
// 迭代器
let iter = arr[Symbol.iterator](); 
console.log(iter); // ArrayIterator {} 
// 执行迭代
console.log(iter.next()); // { done: false, value: 'foo' } 
console.log(iter.next()); // { done: false, value: 'bar' } 
console.log(iter.next()); // { done: true, value: undefined }

2.可迭代对象及可迭代原生对象

2.1可迭代对象(iterable)

什么是可迭代对象呢

它和迭代器是不同的概念;当一个对象实现了iterable protocol协议时,它就是一个可迭代对象; 这个对象的要求是必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性;若一个对象拥有迭代行为,比如在 for...of 中会循环哪些值,那么那个对象便是一个可迭代对象。 一些内置类型,如 Array 或 Map 拥有默认的迭代行为,而其他类型(比如Object)则没有。

当然我们要问一个问题,我们转成这样的一个东西有什么好处呢? 当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作;比如 for...of 操作时,其实就会调用它的 @@iterator 方法;

可迭代对象是一种抽象的说法。 基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序:不过,可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,比如上面提到的计数循环。该循环中生成的值是暂时性的,但循环本身是在执行迭代。计数循环和数组都具有可迭代对象的行为。

任何实现 Iterable 接口的数据结构都可以被实现 Iterator 接口的结构“消费”(consume)。迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。 这种概念上的分离正是 Iterable 和 Iterator 的强大之处。

简单来说可迭代对象实现了迭代器,而迭代器是一次性创建的,每一个迭代器都会关联一个可迭代对象。迭代器是帮助我们对某个数据结构进行遍历的对象。

写法1

var myIterable = {
//yield关键字用来暂停和恢复一个生成器函数
//`yield`在英语里的意思就是“产出”
// 迭代器工厂函数
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

for (let value of myIterable) {
    console.log(value);
}
// 1
// 2
// 3

// 或者

[...myIterable]; // [1, 2, 3]

写法2

const info = {
    friends: ['Michael', 'Stacy', 'Andy'],
    [Symbol.iterator]: function() {
        let index = 0;
        return {
            // next: () => {
            //     let done = (index >= this.friends.length);
            //     let value = !done ? this.friends[index++] : undefined;
            //     return {
            //         done: done,
            //         value: value
            //     };
            // }
            next: () => {
                if (index < this.friends.length) {
                    return {done: false, value: this.friends[index++]};
                } else {
                    return {done: true,value: undefined};
                }
            }
            
        };
    }
};

2.2原生迭代对象

String、Array、Map、Set、arguments对象、NodeListt 等 DOM 集合类型

实例验证

let str = 'abc'; 
let arr = ['a', 'b', 'c']; 
let map = new Map().set('a', 1).set('b', 2).set('c', 3); 
let set = new Set().add('a').add('b').add('c'); 
let els = document.querySelectorAll('div'); 
// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] } 
console.log(arr[Symbol.iterator]); // f values() { [native code] } 
console.log(map[Symbol.iterator]); // f values() { [native code] } 
console.log(set[Symbol.iterator]); // f values() { [native code] } 
console.log(els[Symbol.iterator]); // f values() { [native code] } 
// 调用这个工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator {} 
console.log(arr[Symbol.iterator]()); // ArrayIterator {} 
console.log(map[Symbol.iterator]()); // MapIterator {'a' => 1, 'b' => 2, 'c' => 3}
console.log(set[Symbol.iterator]()); // SetIterator {'a', 'b', 'c'}
console.log(els[Symbol.iterator]()); // ArrayIterator {}

2.3不可迭代类型

number、Object(原生对象)、Boolean、bigint、symbol、undefined、null 其中undefined和null会报错。


let num = 1;
let obj = {a:1}; //原生对象
let bol = true;
let big = 1234567890123456789012345678901234567890n;
let sym = Symbol('sym');
let und = undefined;
let nul = null;
let fun = function(){};//随便定义一个函数是不可迭代

console.log(fun[Symbol.iterator]); // undefined
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
console.log(bol[Symbol.iterator]); // undefined
console.log(big[Symbol.iterator]); // undefined
console.log(sym[Symbol.iterator]); // undefined
console.log(und[Symbol.iterator]); // Uncaught TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
console.log(nul[Symbol.iterator]); // Uncaught TypeError: object null is not iterable (cannot read property Symbol(Symbol.iterator))
console.log(fun[Symbol.iterator]); // undefined

2.4可迭代对象应用

JavaScript中语法:for ...of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment);

创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);

一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);

接收可迭代对象的原生语言特性包括:

for-of 循环

数组解构

扩展操作符

Array.from()

创建集合

创建映射

Promise.all()接收由期约组成的可迭代对象

Promise.race()接收由期约组成的可迭代对象

yield操作符,在生成器中使用(yield 表达式用于委托给另一个generator 或可迭代对象。)

//创建自定义迭代对象
const info = {
    friends: ['Michael', 'Stacy', 'Andy'],
    [Symbol.iterator]: function() {
        let index = 0;
        return {
            next: () => {
                if (index < this.friends.length) {
                    return {done: false, value: this.friends[index++]};
                } else {
                    return {done: true,value: undefined};
                }
            }
        };
    }
};
//或者这么创建也是等价的(结果等价类型不同)
// const info = ['Michael', 'Stacy', 'Andy'];
// console.log(info[Symbol.iterator]().next()); // { done: false, value: 'Michael' }

//for...of循环
for (let friend of info) {
    console.log(friend);
}
// console.log(Array.isArray(info)); // true
//展开运算符
console.log([...info]); // [ 'Michael', 'Stacy', 'Andy' ]
//解构赋值
let [first, second, third] = info;
console.log(first, second, third); // Michael Stacy Andy
//Array.from
console.log(Array.from(info)); // [ 'Michael', 'Stacy', 'Andy' ]
//Set构造函数
console.log(new Set(info)); // Set { 'Michael', 'Stacy', 'Andy' }
//Map构造函数
console.log(new Map([[1, 'one'], [2, 'two'], [3, 'three']])); // Map { 1 => 'one', 2 => 'two', 3 => 'three' }
//Promise.all
Promise.all(info).then((values) => {
    console.log(values); // [ 'Michael', 'Stacy', 'Andy' ]
});

3.自定义迭代器类及终止迭代器

与 Iterable 接口类似,任何实现 Iterator 接口的对象都可以作为迭代器使用。

终止迭代器(退出遍历一个道理) 可选的 return()方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知

道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:

for-of 循环通过 break、continue、return 或 throw 提前退出;

解构操作并未消费所有值。

return()方法必须返回一个有效的 IteratorResult 对象。简单情况下,可以只返回{ done: true }。

因为这个返回值只会用在生成器的上下文中。

class Classroom {
    constructor(name, address, initialStudents) {
      this.name = name
      this.address = address
      this.initialStudents = initialStudents
    }

    *[Symbol.iterator]() {
      yield *this.initialStudents
      // let index = 0
      // return {
      //   next: () => {
      //     if (index < this.initialStudents.length) {
      //       return { done: false, value: this.initialStudents[index++] }
      //     } else {
      //       return { done: true, value: undefined }
      //     }
      //   },
      //   return() {
      //     console.log("迭代器提前终止")
      //     return { done: true }
      //   }
      // }
    }
  }

  const c1 = new Classroom("1101", "3幢222", ["abc", "cba", "nba", "mba"])
  function foo() {
    for (const stu of c1) {
     
      if (stu === "cba") {
        // throw new Error("aaaa")
        // break;
        // return;
        continue;
      }
      console.log(stu)
    }
  }
  foo()

4.其他补充

注意:object不是可迭代对象没有实现迭代器工厂函数

let obj=new Object();
console.log(obj[Symbol.iterator])//undefined

4.1for...in和for...of的区别

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

**总结:**for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

4.2如何使用for...of遍历对象

for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。

如果需要遍历的对象是类数组对象,用Array.from转成数组即可。

var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。

//方法一:
var obj = {
    a:1,
    b:2,
    c:3
};

obj[Symbol.iterator] = function(){
    var keys = Object.keys(this);
    var count = 0;
    return {
        next(){
            if(count<keys.length){
                return {value: obj[keys[count++]],done:false};
            }else{
                return {value:undefined,done:true};
            }
        }
    }
};

for(var k of obj){
    console.log(k);
}


// 方法二
var obj = {
    a:1,
    b:2,
    c:3
};
obj[Symbol.iterator] = function*(){
    var keys = Object.keys(obj);
    for(var k of keys){
        yield [k,obj[k]]
    }
};

for(var [k,v] of obj){
    console.log(k,v);
}

Day62-63【2022年9月24日-2022年9月25日】

休息

精进

真正厉害的人都有强大的内驱力,内在驱动是自我成长的动力,唯有内在驱动,才能无视世界纷扰,永远走在自我成就的路上。

参考资料

  • JavaScript高级程序设计(第4版)
  • MDN
  • 解锁前端面试体系核心攻略
  • 鲨鱼哥面试题总结

结语

志同道合的小伙伴可以加我,一起交流进步,我们坚持每日精进(互相监督思考学习,如果坚持不下来我可以监督你)。我们一起努力鸭! ——>点击这里

备注

按照时间顺序倒叙排列,完结后按时间顺序正序排列方便查看知识点,工作日更新。