Skip to content

S03-11 JS-高级-ES6-内置类

[TOC]

Promise

异步任务的处理

在ES6出来之后,有很多关于Promise的讲解、文章,也有很多经典的书籍讲解Promise

  • 虽然等你学会Promise之后,会觉得Promise不过如此;

  • 但是在初次接触的时候都会觉得这个东西不好理解;

那么这里我从一个实际的例子来作为切入点:

  • 我们调用一个函数,这个函数中发送网络请求(我们可以用定时器来模拟);

  • 如果发送网络请求成功了,那么告知调用者发送成功,并且将相关数据返回过去;

  • 如果发送网络请求失败了,那么告知调用者发送失败,并且告知错误信息;

image-20230620143859708

什么是Promise

在上面的解决方案中,我们确确实实可以解决请求函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题:

  • 第一,我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等;

  • 第二,对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以便可以理解它这个函数到底怎么用;

我们来看一下Promise的API是怎么样的:

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

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

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

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

语法

  • new Promise()(executor)
    • executor(resolve, reject) => void
      • resolve(res),用于将 Promise 状态从pending转换为fulfilled,并返回res。
      • reject(err),用于将 Promise 状态从pending转换为rejected,并返回err。
    • 返回:
    • promisePromise,返回一个新的 Promise 对象。

1、定义

js
const p = new Promise(executor)

cosnt p = new Promise((resolve, reject) => {
  if(成功) {
    // 调用resolve,then传入的回调会被执行
    resolve('成功结果')
  } else {
    // 调用reject,catch传入的回调会被执行
    reject('错误信息')
  }
})

2、使用

js
new Promise(executor).then(onResoledCallback, onRejectedCallback)

new Promise(executor).then((res) => {
  console.log('成功:', res)
},(err) => {
  console.log('失败:', err)
})

new Promise(executor)
  .then(onResoledCallback)
  .catch(onRejectedCallback)

new Promise(executor)
  .then(onResoledCallback)
  .catch(onRejectedCallback)
  .finally(onFinallyCallback)

Promise三种状态

上面Promise使用过程,我们可以将它划分成三个状态

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝;

    • 当执行executor中的代码时,处于该状态;
  • 已兑现(fulfilled): 意味着操作成功完成;

    • 执行了resolve时,处于该状态,Promise已经被兑现;
  • 已拒绝(rejected): 意味着操作失败;

    • 执行了reject时,处于该状态,Promise已经被拒绝;

注意: Promise的状态一旦被确定下来,就不能再执行某个回调函数更改状态

image-20230803160353648

通过Promise重构之前的异步请求

那么有了Promise,我们就可以将之前的代码进行重构了:

image-20230620144006320

executor

executor是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数:

image-20230620144016549

通常我们会在Executor中确定我们的Promise状态:

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

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

这里需要注意:一旦状态被确定下来,Promise的状态会被锁死,该Promise的状态是不可更改的

  • 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成兑现(fulfilled);

  • 在之后我们去调用reject时,已经不会有任何的响应了(并不是这行代码不会执行,而是无法改变Promise状态);

resolve参数类型

resolve不同参数值的区别:

情况一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数

image-20230620144048219

情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态

image-20230620144110509

情况三:如果resolve中传入的是一个thenable对象,这个对象有实现then方法,那么会执行该then方法,并且根据then方法的结果来决定Promise的状态

image-20230620144117642

实例方法

then()

参数

then方法是Promise对象上的一个实例方法

  • 它其实是放在Promise的原型上的 Promise.prototype.then

then方法接受两个参数:

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

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

image-20230620144126129

多次调用

一个Promise的then方法是可以被多次调用的:

  • 每次调用我们都可以传入对应的fulfilled回调;

  • 当Promise的状态变成fulfilled的时候,这些回调函数都会被执行;

image-20230620144135835

返回值@

then方法本身是有返回值的,它的返回值是一个Promise,所以我们可以进行如下的链式调用

image-20230803165403457image-20230803165919371

但是then方法返回的Promise到底处于什么样的状态呢?

Promise有三种状态,那么这个Promise处于什么状态呢?

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

  • 当then方法中的回调函数返回一个结果时

    • 情况一:返回一个普通的值,那么它处于fulfilled状态,并且会将结果作为resolve的参数;

      image-20230803170614371

    • 情况二:返回一个Promise,由新的Promise的状态决定

      image-20230803171156653

    • 情况三:返回一个thenable值;

      image-20230803171446486

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

    image-20230803173524872

catch()

多次调用

catch方法也是Promise对象上的一个实例方法

  • 它也是放在Promise的原型上的 Promise.prototype.catch

一个Promise的catch方法是可以被多次调用的:

  • 每次调用我们都可以传入对应的reject回调;

  • 当Promise的状态变成reject的时候,这些回调函数都会被执行;

image-20230620144204633

返回值

事实上catch方法也是会返回一个Promise对象的,所以catch方法后面我们可以继续调用then方法或者catch方法:

语法:

▸ catch方法也会返回一个新的promise对象

可以通过该promise对象继续调用then()、catch()方法

image-20241104102043410


▸ catch方法的执行时机:

catch方法中的回调函数会被最早的promise的reject()方法回调

image-20241104103210886


▸ catch、then方法返回的默认状态都是fulfilled,后续继续执行then方法

下面的代码中,promise.catch()后续是catch中的err2打印,还是then中的res打印呢?

答案是res打印,这是因为catch传入的回调在执行完后,默认状态依然会是fulfilled的;

image-20241104103646274


▸ 如果希望catch、then方法后续执行catch()方法,那么需要通过抛出一个异常修改返回状态为reject

抛出异常的方法:throw new Error('error message')

image-20241104103917060


▸ 如果抛出的异常后没有catch()方法处理抛出的异常,就会报错

image-20241104105010644

image-20241104105013275


补充: 中断函数继续执行的方法:

  • 方法一:return
  • 方法二:throw new Error()
  • 方法三:yield(暂时中断函数执行)

finally() @ES9

finally是在ES9(ES2018)中新增的一个特性:表示Promise对象无论变成fulfilled还是rejected状态,最终都会被执行的代码。

finally方法的回调是不接收参数的,因为无论前面是fulfilled状态,还是rejected状态,它都会执行。

image-20241104110755128

类方法

resolve()

前面我们学习的then、catch、finally方法都属于Promise的实例方法,都是存放在Promise的prototype上的。

下面我们再来学习一下Promise的类方法

语法:

Promise.resolve的用法相当于new Promise,并且执行resolve操作

image-20230620144302217


▸ resolve参数的形态:

  • 情况一:参数是一个普通的值或者对象

  • 情况二:参数本身是Promise

  • 情况三:参数是一个thenable

使用场景:

▸有时候我们已经有一个现成的内容,希望将其转成Promise来使用,这个时候我们可以使用 Promise.resolve 方法来完成。

image-20241104112126598

reject()

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

语法:

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

image-20230620144323696


▸ Promise.reject传入的参数无论是什么形态,都会直接作为reject状态的参数传递到catch的。

image-20241104112653972

all()

另外一个类方法是Promise.all。

语法:

▸ Promise.all()的作用是将多个Promise包裹在一起形成一个新的Promise

image-20241104114759789


▸ 新的Promise状态由包裹的所有promise共同决定,所有promise执行逻辑与运算

  • 所有的Promise状态变成fulfilled状态时,新Promise状态为fulfilled,并将所有promise的返回值组成一个数组;

    image-20241104114338074

    image-20241104114131164

  • 有一个Promise状态为reject时,新Promise状态为reject,并将第一个reject的返回值作为参数;

    image-20241104114546052

    image-20241104114549524

应用场景: 发送网络请求时,当同时发送多个网络请求后,想等所有请求都有结果再一起返回,可以使用Promise.all

any() @ES12

any方法是ES12中新增的方法,和race方法是类似的:

语法:

▸ any()方法会等到一个fulfilled状态,才会决定新Promise的状态;如果所有的Promise都是reject,那么也会等到所有的Promise都变成rejected状态

image-20241104121343128


▸ any()方法执行逻辑或运算,如果所有的Promise都是reject的,会报AggregateError错误

image-20241104121738763

image-20241104121742381

allSettled() @ES11

all方法有一个缺陷:当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。

  • 那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的;

在ES11(ES2020)中,添加了新的API Promise.allSettled

语法:

▸ Promise.allSettled()方法会在所有的Promise都有结果(settled),无论是fulfilled,还是rejected时,才会有最终的状态;并且结果一定是fulfilled状态

image-20241104115655074

结果:

  • allSettled的结果是一个数组,数组中存放着每一个Promise的结果,并且是对应一个对象的;

  • 这个对象中包含status状态,以及对应的value值;

  • 注意:reject结果是reason值

image-20230803181529408

race()

如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race方法:

race是竞技、竞赛的意思,表示多个Promise相互竞争。

语法:

Promise.race()方法的所有promise谁先有结果,那么就使用谁的结果,无论结果是fulfilled还是rejected

image-20241104120253941

手写-Promise@

Promise结构设计

Promses/A+ 规范: https://promisesaplus.com/

Promise三种状态

image-20230804155236017

调用回调函数时传递参数

image-20230804155855738

实例方法

then
then基本实现

思路:

  • 1 then接收onFulfilledonRejected参数,并将其保存到this上

  • 2.1 在resolve()回调方法的queueMicrotask()回调函数参数中调用onFulfilled方法

  • 2.2 在reject()回调方法的queueMicrotask()回调函数参数中调用onRejected方法

注意: queueMicrotask(cb)方法的作用是将cb回调方法加入到微任务队列中。

image-20230804162214217

同一个promise多次调用then

image-20230804162749533

思路: 将需要多次调用的成功回调和失败回调分别放入一个数组中,调用时再遍历该数组,分别调用数组中的回调方法

1、定义2个数组,将then中的成功、失败回调分别push到这2个数组中

image-20230804164309826

image-20230804164327089

2、遍历这2个数组,再分别调用数组中的回调方法

image-20230804164704305

image-20230804164732409

异步延时调用then

问题: then方法如果在延迟1秒后调用,当promise的resolve()执行时,该then方法的回调函数不会被执行。

image-20230804165009050

分析: 这是因为当promise内部的resolve()执行时,then方法由于延迟原因还没有加入到数组onFulfilledFns,也就不会被执行。

解决: 可以在then方法中,事先判断promise的状态

  • 如果已经是fulfilledrejected,表示已经执行了resolve()rejecct()方法,此时可以直接调用延迟调用的then方法的回调函数。
  • 只有在pending状态才将then方法的回调函数push到数组中保存。

image-20230804165532377


问题: 此时res1、res2无法接收到resolve()执行后的参数

image-20241104154418255

分析: 这是因为执行了resolve()方法后,status立马变成fulfilled,再执行then()方法时status已经处于fulfilled状态,then中的回调会被直接调用,此时queueMicrotask()方法还没有执行,this.value还没有赋值。

解决: 将状态status放入微队列queueMicrotask中

image-20230804165935831

image-20230804170001857


问题: 将状态status放入微队列queueMicrotask中后,resolve和reject都会执行,加入微任务队列

image-20230804171059521

分析: 这是由于resolve()和reject()在加入微任务时,status的状态都为pending。因此都会被加入微任务队列。

解决: 在加入微任务前判断当前状态是否为pending,如果不是pending则表示已经执行了某个回调,就不能加入微任务

image-20230804171504378

image-20230804171611533

then方法的链式调用

image-20230804173822349

思路:

  • 当前then方法没有返回值,所以默认会返回undefined,不能通过undefined.then()链式调用方法。
  • 通过then方法中返回一个新的Promise,可以实现链式调用then方法
  • 新Promise中resolve(res)或reject(err)的参数res或err必须是上一次then中回调返回的结果

image-20241104165759021


问题: 在new Promise中抛出异常的情况

image-20230804174130505

解决: 在constructor中捕获执行executor()的异常。

image-20230804174247394

封装try catch中相似的代码

image-20230804174556340

image-20230804175948146

then回调函数参数可选【
then执行结果值类型【

判断下面result的类型:普通值、promise、thenable

补充:

  • 可以通过result instanceof Promise判断否是一个Promise
  • 可以通过typeof result.then === 'function'判断是否是一个thenable对象

image-20230804175249845

catch

调用catch方法

image-20241104171857387


▸ 基本实现

思路:通过调用then方法时只传递reject回调实现catch

image-20241104171817256


问题: 在回调函数有值(存在)的情况下,才去执行函数或添加到数组中

image-20230804180124250


问题: 调用catch的是返回的新promise,不是和then同一个promise

image-20241104172951614

解决: 当promise1中的reject为undefined时,在then方法执行reject回调处抛出一个异常。这样就会被第二个promise接收到了

image-20230804181618191

finally

调用finally方法

image-20230804203232552


▸ 基本实现

思路: 可以借用then()方法,在then方法的resolve和reject回调中都调用onfinally()实现

image-20241104175809777


问题: 添加catch后,执行resolve时,finally被阻止了,不再执行finally中的回调。只有执行reject时才会执行finally

image-20230804203602679

image-20230804203353587

原因: 这是由于catch方法中是这样调用then的:this.then(undefined, onRejected),其中成功回调是undefined,所以就不会处理上次then返回的值

image-20241104180538179

解决: 当onFulfilled为undefined时,给它一个默认的回调函数:value => { return value }

image-20230804204302336

类方法

resolve

思路: 直接在 new Promise()中调用 resolve() 方法

image-20241105220625982

使用resolve

image-20230804204953824

reject

思路: 直接在 new Promise()中调用 reject() 方法

image-20241105220720564

使用reject

image-20230804205018805

all

特点:

  • all中所有的promise都有结果后才会执行then或catch方法
  • 所有的promise之间执行与运算:都为resolve进入then方法;有一个reject进入catch方法

关键: 什么时候要执行resolve、什么时候要执行reject

思路: 遍历all的promises参数

  • 当有一个promise结果为reject时直接执行reject(),
  • 否则进入then,并保存结果到values中,当所有promise都有结果并且结果为resolve时,执行resolve()

image-20241105222517652

使用all

image-20241105221817643

allSettled

特性: allSettled会等所有promise都有结果(不区分resolve和reject),进入then方法,不会进入catch方法

image-20241105223847145

使用allSettled

image-20241105223724491

image-20230804211020865

race

特性: 只要有一个promise有结果,race立马有结果,无论resolve还是reject

image-20241105224634663

等价于下面的写法:

image-20230804211736445

使用race

image-20230804211613090

image-20230804211633864

any

特性:

  • 必须等到promise有一个resolve的结果,any才会有一个resolve的结果
  • 否则必须等到所有的promise都为reject结果,any才会有一个reject的结果

image-20241105230012823

使用any

image-20241105230148786

image-20241105230400826

image-20230804212626577

image-20230804212634453

最终代码@

js
  /* 工具函数-封装try...catch函数 */
  function runFunctionWithCatchError(fn, value, resolve, reject) {
    try {
      resolve(fn(value))
    } catch (err) {
      reject(err)
    }
  }

  // Promise状态
  const PROMISE_STATUS_PENDING = 'pending'
  const PROMISE_STATUS_FULFILLED = 'fulfilled'
  const PROMISE_STATUS_REJECTED = 'rejected'

  class MrPromise {
    constructor(executor) {
      this.status = PROMISE_STATUS_PENDING
      this.value = undefined
      this.reason = undefined
      this.onFulfilledFns = []
      this.onRejectedFns = []

      const resolve = (value) => {
        if (this.status === PROMISE_STATUS_PENDING) {
          queueMicrotask(() => {
            if (this.status !== PROMISE_STATUS_PENDING) return
            this.status = PROMISE_STATUS_FULFILLED
            this.value = value
            for (const fn of this.onFulfilledFns) {
              fn(this.value)
            }
          })
        }
      }

      const reject = (reason) => {
        if (this.status === PROMISE_STATUS_PENDING) {
          queueMicrotask(() => {
            if (this.status !== PROMISE_STATUS_PENDING) return
            this.status = PROMISE_STATUS_REJECTED
            this.reason = reason
            for (const fn of this.onRejectedFns) {
              fn(this.reason)
            }
          })
        }
      }

      try {
        executor(resolve, reject)
      } catch (err) {
        reject(err)
      }
    }

    then(onFulfilled, onRejected) {
      // 判断onFulfilled、onRejected回调函数是否存在
      onRejected = onRejected || ((err) => { throw err })
      onFulfilled = onFulfilled || ((res) => res)
      
      return new MrPromise((resolve, reject) => {
        // console.log('then status: ', this.status)
        if (this.status === PROMISE_STATUS_FULFILLED) {
          runFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
        }
        if (this.status === PROMISE_STATUS_REJECTED) {
          runFunctionWithCatchError(onRejected, this.reason, resolve, reject)
        }

        if (this.status === PROMISE_STATUS_PENDING) {
          this.onFulfilledFns.push(() => {
            runFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
          })

          this.onRejectedFns.push(() => {
            runFunctionWithCatchError(onRejected, this.reason, resolve, reject)
          })
        }
      })
    }

    catch(onRejected) {
      return this.then(undefined, onRejected)
    }

    finally(onFinally) {
      this.then(
        () => {
          onFinally()
        },
        () => {
          onFinally()
        }
      )
    }

    static resolve(value) {
      return new Promise((resolve) => resolve(value))
    }

    static reject(reason) {
      return new Promise((resolve, reject) => reject(reason))
    }

    static all(promises) {
      return new Promise((resolve, reject) => {
        const values = []
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              values.push(res)
              if (values.length === promises.length) {
                resolve(values)
              }
            },
            (err) => {
              reject(err)
            }
          )
        })
      })
    }

    static allSettled(promises) {
      return new Promise((resolve, reject) => {
        const results = []
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              results.push({ status: 'fulfilled', value: res })
              if (results.length === promises.length) {
                resolve(results)
              }
            },
            (err) => {
              results.push({ status: 'rejected', reason: err })
              if (results.length === promises.length) {
                resolve(results)
              }
            }
          )
        })
      })
    }

    static race(promises) {
      return new Promise((resolve, reject) => {
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              resolve(res)
            },
            (err) => {
              reject(err)
            }
          )
        })
      })
    }

    static any(promises) {
      return new Promise((resolve, reject) => {
        const reasons = []
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              resolve(res)
            },
            (err) => {
              reasons.push(err)
              if (reasons.length === promises.length) {
                reject(new AggregateError(err))
              }
            }
          )
        })
      })
    }
  }

测试

js
  // 测试
  const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('p1~')
    }, 3000)
  })
  const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('p2~')
    }, 5000)
  })
  const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('p3~')
    }, 3000)
  })

  const p = new MrPromise((resolve, reject) => {
    // throw new Error('抛出异常')

    resolve('aaa')
    // reject('111')

    // setTimeout(() => {
    //   // resolve('aaa')
    //   reject('111')
    // }, 1000)
  })

  // - 类方法-any
  Promise.any([p1, p2, p3]).then(
    (res) => {
      console.log('any res: ', res)
    },
    (err) => {
      console.log('any err: ', err)
    }
  )

  // // - 类方法-race
  // Promise.race([p1, p2, p3]).then(
  //   (res) => {
  //     console.log('race res: ', res)
  //   },
  //   (err) => {
  //     console.log('race err: ', err)
  //   }
  // )

  // // - 类方法-allSettled
  // Promise.allSettled([p1, p2, p3]).then((res) => {
  //   console.log('allSettled: ', res)
  // })

  // // - 类方法-all
  // Promise.all([p1, p2, p3]).then(
  //   (res) => {
  //     console.log('all res: ', res)
  //   },
  //   (err) => {
  //     console.log('all err: ', err)
  //   }
  // )

  // // - 类方法-reject
  // Promise.reject('222').catch((err) => {
  //   console.log(err)
  // })

  // // - 类方法-resolve
  // Promise.resolve('1111').then((res) => {
  //   console.log(res)
  // })

  // p.then(
  //   (res) => {
  //     console.log('res: ', res)
  //   },
  //   (err) => {
  //     console.log('err: ', err)
  //   }
  // )

  // // - 异步延迟调用
  // setTimeout(() => {
  //   p.then(
  //     (res) => {
  //       console.log('异步延时调用 res: ', res)
  //     },
  //     (err) => {
  //       console.log('异步延时调用 err: ', err)
  //     }
  //   )
  // }, 2000)

  // // - 链式调用
  // p.then(
  //   (res) => {
  //     console.log('链式调用 res1: ', res)
  //     return 'bbb'
  //   },
  //   (err) => {
  //     console.log('链式调用 err1: ', err)
  //     return '222'
  //   }
  // ).then(
  //   (res) => {
  //     console.log('链式调用 res2: ', res)
  //     return 'ccc'
  //   },
  //   (err) => {
  //     console.log('链式调用 err2: ', err)
  //     return '333'
  //   }
  // )

  // // - catch
  // p.then((res) => {
  //   console.log('then res: ', res)
  // }).catch((err) => {
  //   console.log('catch err: ', err)
  // })

  // // - finally
  // p.then((res) => {
  //   console.log('then res: ', res)
  // })
  //   .catch((err) => {
  //     console.log('catch err: ', err)
  //   })
  //   .finally(() => {
  //     console.log('finally~')
  //   })

  // p.then(
  //   (res) => {
  //     console.log('res: ', res)
  //   },
  //   (err) => {
  //     console.log('err: ', err)
  //   }
  // )
  // p.then(
  //   (res) => {
  //     console.log('res2: ', res)
  //   },
  //   (err) => {
  //     console.log('err2: ', err)
  //   }
  // )

Symbol

API

  • 构造函数
  • Symbol()(description?),创建一个 Symbol
  • 属性
  • Symbol.iterator() => iterator,用于定义一个对象如何被迭代,可以让一个对象成为“可迭代”的对象。返回一个迭代器对象。
  • Symbol.prototype.descriptionstring,(只读),返回 Symbol 对象的可选描述的字符串。
  • 方法
  • Symbol.for()(key),会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol。
  • Symbol.keyFor(sym):``,用来获取全局 symbol 注册表中与某个 symbol 关联的键 key。
  • 相关方法
  • Object.getOwnPropertySymbols()(obj),返回一个给定对象自身的所有 Symbol 属性的数组。

构造函数

  • Symbol()(description?),用于创建一个 Symbol对象。主要用于对象的属性名,可以避免属性名冲突。

    • description?string,对 symbol 的描述,可用于调试但不是访问 symbol 本身,不影响符号的唯一性。

    • 返回:

    • symSymbol,返回一个Symbol对象。

    • js
      // 基本使用
      const symbol1 = Symbol();
      const symbol2 = Symbol('description');
      console.log(symbol1); // Symbol()
      console.log(symbol2); // Symbol(description)
      
      // 作为对象属性名
      const mySymbol = Symbol('myUniqueKey');
      const obj = {
        [mySymbol]: 'value'
      };
      console.log(obj[mySymbol]); // 输出:'value'

属性【

  • Symbol.iterator() => iterator,用于定义一个对象如何被迭代,可以让一个对象成为“可迭代”的对象。返回一个迭代器对象。
  • Symbol.prototype.descriptionstring,(只读),返回 Symbol 对象的可选描述的字符串。

方法【

  • Symbol.for()(key),会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol。
  • Symbol.keyFor(sym):``,用来获取全局 symbol 注册表中与某个 symbol 关联的键 key。

相关方法【

  • Object.getOwnPropertySymbols()(obj),返回一个给定对象自身的所有 Symbol 属性的数组。

痛点:

那么为什么需要 Symbol 呢?

  • 在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
  • 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
  • 比如我们前面在讲 apply、call、bind 实现时,我们有给其中添加一个 fn 属性,那么如果它内部原来已经有了 fn 属性了呢?
  • 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;

Symbol:

Symbol 是什么呢?Symbol 是 ES6 中新增的一个基本数据类型,翻译为符号

Symbol 就是为了解决上面的问题,用来生成一个独一无二的值

  • Symbol 值是通过 Symbol() 函数来生成的,生成后可以作为属性名;这是该数据类型仅有的目的
  • 也就是在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值

特性:

  • Symbol 函数执行后每次创建出来的值都是独一无二的。
  • 可以在创建 Symbol 值的时候传入一个描述 description(ES10新增特性)。

image-20230228160159938

语法

js
Symbol(description?)

参数

  • descriptionstring,对 symbol 的描述,可用于调试但不是访问 symbol 本身

示例

js
// 1. 通过Symbol函数创建一个Symbol
const s1 = Symbol();

// 2. 创建的时候传入一个description
const s2 = Symbol("s2");

// 3. Symbol函数每次创建出来的值都是独一无二的
console.log(Symbol() == Symbol()); // falses
console.log(Symbol() === Symbol()); // false

// 4. Symbol作为对象属性的标识符
const obj = {
  [s1]: "name",
  [s2]: "age",
};
console.log(obj); // {Symbol(): 'name', Symbol(s2): 'age'}

// 5. 获取Symbol对应的key
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(), Symbol(s2)]

// 6. Symbol.for(key)
const s3 = Symbol.for("s3");
console.log(s3); // Symbol(s3)

// 7. 相同的key,通过`Symbol.for()`可以生成相同的Symbol值
const s4 = Symbol.for("ss");
const s5 = Symbol.for("ss");
console.log(s4 === s5); // true

// 8. 通过`Symbol.keyFor()` 可以获取通过Symbol.for()传入的key
console.log(Symbol.keyFor(s2)); // undefined
console.log(Symbol.keyFor(s5)); // ss

Symbol 作为属性名

我们通常会使用 Symbol 在对象中表示唯一的属性名

image-20230228153141689

相同值的 Symbol

前面我们讲 Symbol 的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的 Symbol应该怎么来做呢?

  • 我们可以使用 Symbol.for 方法来做到这一点
  • 并且我们可以通过 Symbol.keyFor 方法来获取对应的 key

image-20230228153211971

相同的 key,通过Symbol.for()可以生成相同的 Symbol 值

js
const s4 = Symbol.for("ss");
const s5 = Symbol.for("ss");
console.log(s4 === s5); // true

通过Symbol.keyFor() 可以获取通过 Symbol.for()传入的 key

js
console.log(Symbol.keyFor(s2)); // undefined
console.log(Symbol.keyFor(s5)); // ss

Set

API

  • 属性
  • size:``,返回 Set 中元素的个数
  • 方法
  • add(value)返回:Set对象,添加某个元素
  • delete(value)返回:Boolean,从 set 中删除和这个值相等的元素
  • has(value)返回:Boolean,判断 set 中是否存在某个元素
  • clear()返回:void,清空 set 中所有的元素
  • forEach(callback, thisArg?)返回:undefined,通过 forEach 遍历 set
    • 参数
    • callbackfunction(value?, key?, set?),为集合中每个元素执行的回调函数
    • thisArg,在执行 callback 时作为 this 使用

注意:Set 支持 for of 的遍历

常见方法

添加元素

js
// 2. 添加Set - add()
set.add("Tom");
console.log(set); // Set(1) {'Tom'}

// 3. Set中不能放入重复的元素
set.add("Jack");
set.add("Jack");
console.log(set); // Set(2) {'Tom', 'Jack'}

删除元素

js
// 5. 常见方法 - delete()
console.log(set); // Set(2) {'Tom', 'Jack'}
set.delete("Tom");
console.log(set); // Set(1) {'Jack'}

是否包含某个元素

js
// 6. 常见方法 - has()
console.log(set.has("Jack")); // true

清空 set

js
// 7. 常见方法 - clear()
set.clear();
console.log(set); // Set(0) {size: 0}

forEach 遍历

js
// 8. 常见方法 - forEach()
set2.forEach((item, index, set) => {
  console.log(item, index, set); // 刘备 刘备 Set(4) {'刘备', '关羽', '张飞', '吕布'}
});

语法

Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。

js
new Set(iterable?)

参数

  • iterable:``,如果传递一个可迭代对象,它的所有元素将不重复地被添加到新的 Set 中

返回值

  • 一个新的 Set 对象

示例

js
const mySet = new Set();

mySet.add(1); // Set [ 1 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add("some text"); // Set [ 1, 5, 'some text' ]
const o = { a: 1, b: 2 };
mySet.add(o);

基本使用

在 ES6 之前,我们存储数据的结构主要有两种:数组、对象。

  • 在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式 WeakSet、WeakMap。

Set 是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复

  • 创建 Set 我们需要通过 Set 构造函数(暂时没有字面量创建的方式):

我们可以发现 Set 中存放的元素是不会重复的,那么 Set 有一个非常常用的功能就是给数组去重

创建 Set

js
// 1. 创建Set
const set = new Set();
console.log(set); // Set(0) {size: 0}

2 个空对象不是重复的元素

js
// 10. 2个空对象不是重复的元素
const set4 = new Set();
set4.add({});
set4.add({});
console.log(set4); // Set(2) {{…}, {…}}

应用:数组去重

js
// 4. 应用:数组去重
const arr = ["刘备", "关羽", "张飞", "吕布", "关羽", "刘备"];
const set2 = new Set(arr);
console.log(set2); // Set(4) {'刘备', '关羽', '张飞', '吕布'}
const set3 = Array.from(set2);
console.log(set3); // (4) ['刘备', '关羽', '张飞', '吕布']

// 简单写法一
console.log(Array.from(new Set(arr))); // (4) ['刘备', '关羽', '张飞', '吕布']
// 或者写法二
console.log([...new Set(arr)]); // (4) ['刘备', '关羽', '张飞', '吕布']

之前数组去重的做法

image-20230228174503687

set 支持 for...of 遍历

只要是可迭代对象都可以通过 for...of 遍历

js
// 9. 通过for...of遍历Set
for (const item of set2) {
  console.log(item); // 刘备 关羽 张飞 吕布
}

WeakSet

API

  • 方法
  • add(value)返回:WeakSet对象,添加某个元素
  • delete(value)返回:Boolean,从 WeakSet 中删除和这个值相等的元素
  • has(value)返回:Boolean,判断 WeakSet 中是否存在某个元素

语法

WeakSet 对象允许你将弱保持对象存储在一个集合中

和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。

语法

js
const ws = new WeakSet(iterable?)

参数:

  • iterable: 可迭代对象

特性:

  • WeakSet内部的元素不能重复

基本使用

和 Set 类似的另外一个数据结构称之为 WeakSet,也是内部元素不能重复的数据结构

那么和 Set有什么区别呢?

  • 区别一:WeakSet 中只能存放对象类型,不能存放基本数据类型;
  • 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收;

WeakSet 中只能存放对象类型

image-20230228153508949

普通对象的内存图

解释:普通对象被重新赋值为 null 时,就断开了和内存中对象的联系,但是由于之前已经将对象的内存地址赋值给了数组 arr,赋值为 null 后这些对象依然被数组 arr 所引用,所以它们并不会被销毁

image-20230301095326167

WeakSet 内存图

解释: 添加到 WeakSet 中的对象都是弱引用,可能会被 GC 随时回收

image-20230301100631217

注意:WeakSet 不能遍历

  • 因为 WeakSet 只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。

  • 所以存储到 WeakSet 中的对象是没办法获取的;

应用:限制类中方法的调用者

  • 事实上这个问题并不好回答,我们来使用一个 Stack Overflow 上的答案;

此处用 WeakSet 的好处:想要销毁实例对象 p 的时候,可以直接通过p = null 销毁,如果使用 Set 的话,由于实例对象一直被 Set 引用,所以无法销毁

image-20230301101613022

image-20230228153607838

Map

API

Map常见的属性:

  • size:返回Map中元素的个数;

Map常见的方法:

  • set(key, value):在Map中添加key、value,并且返回整个Map对象;

  • get(key):根据key获取Map中的value;

  • has(key):判断是否包括某一个key,返回Boolean类型;

  • delete(key):根据key删除一个键值对,返回Boolean类型;

  • clear():清空所有的元素;

  • forEach(callback, [, thisArg]):通过forEach遍历Map;

Map也可以通过for of进行遍历。

示例:

image-20230802144837770

Map的基本使用

另外一个新增的数据结构Map,用于存储映射关系

但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?

  • 事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);

  • 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;

那么我们就可以使用Map:

语法

js
const m = new Map(iterable?)

参数:

  • iterable:可迭代对象

Map和对象的区别

  • 属性名: 对象存储时只能使用字符串Symbol作为属性名;而Map可以使用任何值(包括对象和基本类型)作为属性名

image-20230620141659357

image-20230620141706178

特性: Map中不能存储包含相同键的元素,可以存储包含相同值得元素

js
  const m4 = new Map([
    [1, "tom"],
    [1, "jack"],
    ["name", "jerry"],
    ["nick", "jerry"],
  ]);
  console.log("m4: ", m4); // => {1 => 'jack', 'name' => 'jerry', 'nick' => 'jerry'}

WeakMap

API

  • set(key, value):在WeakMap中添加key、value,并且返回整个Map对象;

  • get(key):根据key获取Map中的value;

  • has(key):判断是否包括某一个key,返回Boolean类型;

  • delete(key):根据key删除一个键值对,返回Boolean类型;

WeakMap的使用

和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。

语法:

js
const wm = new WeakMap(iterable?)

参数:

  • iterable:可迭代对象

WeakMap和Map的区别:

  • 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;

  • 区别二:WeakMap的key对对象的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;

image-20230620141731766

WeakMap的应用

注意:WeakMap也是不能遍历的

  • 没有forEach方法,也不支持通过for of的方式进行遍历;

那么我们的WeakMap有什么作用呢?(后续专门讲解)

image-20230620141751908

Proxy@

API【

监听对象属性的操作

需求: 有一个对象,我们希望监听这个对象中的属性被设置或获取的过程

  • 通过我们前面所学的知识,能不能做到这一点呢?

思路: 我们可以通过之前的属性描述符中的存储属性描述符来做到;

image-20230620143125043

上边这段代码就利用了前面讲过的 Object.defineProperty 的存储属性描述符来对属性的操作进行监听。

缺点:

但是这样做有什么缺点呢?

  • 首先,Object.defineProperty设计初衷不是为了去监听截止一个对象中所有的属性的。

    • 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
  • 其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。

所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。

Proxy基本使用

ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:

  • 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);

  • 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作

我们可以将上面的案例用Proxy来实现一次:

  • 首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler;

  • 其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;

语法:

js
const p= new Proxy(target, handler)

参数:

  • target,要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  • handler,一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p的行为

示例: 基本使用

image-20230802215042788

Proxy的set和get捕获器

如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕获器(Trap):

set和get分别对应的是函数类型;

  • set函数有四个参数:

    • target:目标对象(侦听的对象);
    • property:将被设置的属性key;
    • value:新属性值;
    • receiver:调用的代理对象;
  • get函数有三个参数:

    • target:目标对象(侦听的对象);
    • property:被获取的属性key;
    • receiver:调用的代理对象;

image-20230620143202617

Proxy所有捕获器

13个捕获器分别是做什么的呢?

  • 监听普通对象
  • handler.get(target, prop, receiver?)obj.name,获取属性值
  • handler.set(target, prop, newValue, receiver?)obj.name = 'mr',设置属性值
  • handler.has(target, prop)in, 判断是否存在某属性
  • handler.defineProperty(target, prop, descriptor)Object.defineProperty, 设置属性描述符
  • handler.deleteProperty(target, prop)delete,删除属性
  • 监听函数对象
  • handler.apply(target, thisArg, args)Object.prototype.apply,函数调用
  • handler.construct(target, args, newTarget?)new,调用构造函数
  • handler.getPrototypeOf(target)Object.getPrototypeOf, 获取对象的原型
  • handler.setPrototypeOf(target, prototype)Object.setPrototypeOf, 设置对象的原型
  • handler.isExtensible(target)Object.isExtensible, 判断是否可以新增属性
  • handler.preventExtensions(target)Object.preventExtensions, 阻止对象扩展
  • handler.ownKeys(target)Object.getOwnPropertyNamesObject.getOwnPropertySymbols ,获取自身上的所有属性
  • handler.getOwnPropertyDescriptor(target, prop)Object.getOwnPropertyDescriptor, 获取自身上的属性描述符

示例:

image-20230803110138914

Proxy监听函数对象

当然,我们还会看到捕捉器中还有constructapply,它们是应用于监听函数对象的:

image-20230620143600188

Reflect@

API

Reflect中有哪些常见的方法呢?它和Proxy是一一对应的,也是13个

  • 监听普通对象
  • Reflect.get(target, prop, receiver?)obj.name,获取属性值
  • Reflect.set(target, prop, newValue, receiver?)obj.name = 'mr',设置属性值
  • Reflect.has(target, prop)in, 判断是否存在某属性
  • Reflect.defineProperty(target, prop, descriptor)Object.defineProperty, 设置属性描述符
  • Reflect.deleteProperty(target, prop)delete,删除属性
  • 监听函数对象
  • Reflect.apply(target, thisArg, args)Object.prototype.apply,函数调用
  • Reflect.construct(target, args, newTarget?)new,调用构造函数
  • Reflect.getPrototypeOf(target)Object.getPrototypeOf, 获取对象的原型
  • Reflect.setPrototypeOf(target, prototype)Object.setPrototypeOf, 设置对象的原型
  • Reflect.isExtensible(target)Object.isExtensible, 判断是否可以新增属性
  • Reflect.preventExtensions(target)Object.preventExtensions, 阻止对象扩展
  • Reflect.ownKeys(target)Object.getOwnPropertyNamesObject.getOwnPropertySymbols ,获取自身上的所有属性
  • Reflect.getOwnPropertyDescriptor(target, prop)Object.getOwnPropertyDescriptor, 获取自身上的属性描述符

Reflect的作用

Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射

作用:

那么这个Reflect有什么用呢?

  • 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法

  • 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();

  • 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;

如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?

  • 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;

  • 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;

  • 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;

  • 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上

  • 另外在使用Proxy时,可以做到不操作原对象

那么Object和Reflect对象之间的API关系,可以参考MDN文档:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/Comparing_Reflect_and_Object_methods

Reflect的使用

那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:

image-20230620143756226

优点: 使用Reflect结合Proxy代理对象的优点

  • 优点一:代理对象的目的:实现不再直接操作原对象
  • 优点二:Reflect.set方法有返回布尔值,可以判断本次操作是否成功
  • 优点三:receiver就是外层Proxy对象。Reflect.set/get的最后一个参数receiver可以决定对象访问器settter/getter的this指向

Receiver的作用

我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?

  • 如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this;

我们来看这样的一个对象:

image-20230620143807625

image-20230803141510347

Reflect的construct

image-20230620143816629

Iterator@

API

iterator({next(), return()?}),JS中的迭代器,实现了next方法的对象,在next方法中必须返回一个包含了done和value属性的对象

  • next()(arg?),返回一个包含 valuedone 属性的对象。

    • arg?:``,可选参数。
    • 返回: {done, value}
    • doneboolean,是否已经遍历完所有元素。
      • false:表示迭代未完成,value值为any;
      • true:表示迭代完毕,value值为undefined
    • valueany | undefined,表示当前元素的值。
  • return()?(value),结束当前的迭代,并返回一个包含 done 属性的对象。

    • valueany,是 return() 方法返回的值,可以是任何类型的值。通常用来返回一个终止值,标识迭代的结束。
    • 返回: {done, value?}
    • doneboolean,表示迭代是否完成。调用 return() 时,这个属性的值通常是 true
    • value?any,返回迭代结束时的值。可以用来返回终止时需要返回的值。
  • js
    const customIterator = {
      items: [1, 2, 3, 4],
      index: 0,
      
      next() {
        if (this.index < this.items.length) {
          return { value: this.items[this.index++], done: false };
        } else {
        	return { value: undefined, done: true };  
        }
      },
    
      return(value) {
        console.log("Iteration is being stopped early!");
        return { value: value, done: true };
      }
    };
    
    const iter = customIterator;
    
    console.log(iter.next()); // { value: 1, done: false }
    console.log(iter.next()); // { value: 2, done: false }
    
    console.log(iter.return(99)); // Iteration is being stopped early! { value: 99, done: true }

概念:

  • 迭代器:Iterator。它是一种提供遍历集合元素的机制。
  • 可迭代对象:Iterable。它是一种实现了Iterator Protocol方法的对象。
  • 迭代器协议:Iterator Protocol。它是一种定义迭代器对象的标准,它规定了迭代器对象必须实现的方法和属性。

迭代器

介绍

迭代器iterator),帮助用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。

  • 行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;

  • 在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;

从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。

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

迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;在JavaScript中这个标准就是一个特定的next方法;

next方法有如下的要求:

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

  • done(boolean)

    • 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
    • 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
  • value

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

语法

JS中的迭代器实现了next方法的对象,在next方法中必须返回一个包含了done和value属性的对象

js
const arr = ['a', 'b', 'c']

let index = 0
const iterator = {
    next: function() {
        if(index < arr.length) {
            return {done: false, vlaue: arr[index++]}
        } else {
            return {done: true, value: undefined}
        }
    }
}

示例: 迭代器的代码练习

1、迭代器-基本案例

image-20230620144529302

2、迭代器-封装一个通用的迭代器生成函数

image-20230620144536523

可迭代对象

介绍

但是上面的代码整体来说看起来是有点奇怪的:

  • 我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;

  • 事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;

什么又是可迭代对象呢?

  • 它和迭代器是不同的概念;

  • 当一个对象实现了iterable protocol协议时,它就是一个可迭代对象

  • 这个对象的要求是必须实现 @@iterator() 方法,在代码中我们使用 Symbol.iterator 访问该属性;

当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?

  • 当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作

  • 比如 for...of 操作时,其实就会调用它的 @@iterator 方法

特性:

1、可迭代对象内部需要实现[Symbol.iterator] 方法,该方法返回一个迭代器。

image-20230809121146412

image-20230809135531910

2、可迭代对象可以进行for...of遍历

image-20230809135442149

优化

1、迭代infos中的friends属性

next函数用箭头函数书写,可以让其内部的this指向可迭代对象,从而实现更通用的封装

image-20230809140740310

2、迭代对象中的键,值,键值对

image-20230809141505683

image-20230809141509393

内置可迭代对象

事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个可迭代对象的:

  • StringArrayMapSetarguments对象、NodeList集合;

用法:

▸ 数组: arr[Symbol.iterator]()

image-20241111110206474


▸ Set: set[Symbol.iterator]()

image-20241111110353300


▸ arguments: arguments[Symbol.iterator]()

image-20241111110604983

应用

那么这些东西可以被用在哪里呢?

  • JS中语法

    • for ...of遍历

    • `...:展开语法(spread syntax)

    • yield*(后面讲)

    • [name, age] = arr解构赋值(Destructuring_assignment)

  • 创建一些对象时,可传入可迭代对象

    • new Map()(iterable?),创建Map对象
    • new WeakMap()(iterable?),创建WeakMap对象
    • new Set()(iterable?),创建Set对象
    • new WeakSet()(iterable?),创建WeakSet对象
  • 一些方法的调用

    • Promise.all()(iterable)
    • Promise.race()(iterable)
    • Array.from()(iterable)

image-20230620144624921

image-20230620144647185

image-20241111112455287

自定义类的迭代

在前面我们看到Array、Set、String、Map等类创建出来的对象都是可迭代对象:

  • 在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象:

  • 如果我们也希望自定义类创建出来的对象默认是可迭代对象,那么在设计类的时候我们就可以添加上 @@iterator 方法

案例:创建一个classroom的类

  • 教室中有自己的位置、名称、当前教室的学生;

  • 这个教室可以进来新学生(push);

  • 创建的教室对象是可迭代对象;

自定义类的迭代实现: 自定义类创建出来的对象默认是可迭代对象

image-20230620144706396

image-20230620144716419

迭代器-监听中断

iterator({next(), return()?}),JS中的迭代器,实现了next方法的对象,在next方法中必须返回一个包含了done和value属性的对象

  • next()(arg?),返回一个包含 valuedone 属性的对象。

    • arg?:``,可选参数。
    • 返回: {done, value}
    • doneboolean,是否已经遍历完所有元素。
      • false:表示迭代未完成,value值为any;
      • true:表示迭代完毕,value值为undefined
    • valueany | undefined,表示当前元素的值。
  • return()?(value),结束当前的迭代,并返回一个包含 done 属性的对象。

    • valueany,是 return() 方法返回的值,可以是任何类型的值。通常用来返回一个终止值,标识迭代的结束。
    • 返回: {done, value?}
    • doneboolean,表示迭代是否完成。调用 return() 时,这个属性的值通常是 true
    • value?any,返回迭代结束时的值。可以用来返回终止时需要返回的值。
  • js
    const customIterator = {
      items: [1, 2, 3, 4],
      index: 0,
      
      next() {
        if (this.index < this.items.length) {
          return { value: this.items[this.index++], done: false };
        } else {
        	return { value: undefined, done: true };  
        }
      },
    
      return(value) {
        console.log("Iteration is being stopped early!");
        return { value: value, done: true };
      }
    };
    
    const iter = customIterator;
    
    console.log(iter.next()); // { value: 1, done: false }
    console.log(iter.next()); // { value: 2, done: false }
    
    console.log(iter.return(99)); // Iteration is being stopped early! { value: 99, done: true }

迭代器在某些情况下会在没有完全迭代的情况下中断:

  • 比如遍历的过程中通过breakreturnthrow中断了循环操作;

  • 比如在解构的时候,没有解构所有的值

那么这个时候我们想要监听中断的话,可以添加 return()方法:

image-20230620144726759

image-20230620144737229

Generator@

概念

生成器: 是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。

  • 平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。

生成器函数: 也是一个函数,但是和普通的函数有一些区别:

  • 首先,生成器函数需要在function的后面加一个符号:*function* foo() 或者function *foo()

  • 其次,生成器函数可以通过yield关键字来控制函数的执行流程

  • 最后,生成器函数执行时返回一个Generator(生成器):

    • 要想执行函数内部的代码,需要通过生成器对象调用它的next方法
    • 当遇到yield时,就会中断执行,需要再次调用next方法,才会继续执行
  • 生成器事实上是一种特殊的迭代器

    • MDN:Instead, they return a special type of iterator, called a Generator.

生成器函数

我们发现下面的生成器函数foo的执行体压根没有执行,它只是返回了一个生成器对象

  • 那么我们如何可以让它执行函数中的东西呢?调用next即可;

  • 我们之前学习迭代器时,知道迭代器的next是会有返回值的;

  • 但是我们很多时候不希望next返回的是一个undefined,这个时候我们可以通过yield来返回结果

image-20230620144819474

语法

生成器函数是 JavaScript 中的一种特殊函数类型,它通过 function* 语法定义,具有多次返回值的能力,可以暂停和恢复执行。

语法:

js
function* generatorFunction() {
  // 生成器逻辑
}
  • function*:生成器函数使用 function* 来定义。可以在执行过程中暂停,并且可以多次返回值。
  • 生成器函数默认在执行时,返回一个生成器对象。
  • yield:表示生成器函数在此暂停,并返回一个值。直到外部调用 next() 方法。
  • next():生成器函数返回的是一个生成器对象,该对象具有 next() 方法。调用 next() 方法时,生成器从上次暂停的地方继续执行,直到遇到下一个 yield 或结束。
  • return():可以提前终止生成器并返回一个值。如果在 yield 后调用 return(),生成器将不再返回任何值,done 会变为 true
  • yield*:是一个委托表达式,用于将生成器的执行委托给另一个生成器。它会一次性执行另一个生成器,返回它的所有值。

用法:

▸ 基本使用

js
// 1. 定义一个生成器函数
function* foo() {
    console.log('1111')
    console.log('2222')
    yield
    console.log('3333')
    console.log('4444')
    yield
    console.log('5555')
    console.log('6666')
}

// 2. 调用生成器函数,返回一个生成器对象
const gen = foo();

// 3. 调用生成器对象的next方法
console.log(gen.next());  // 1111, 2222
console.log(gen.next());  // 3333, 4444
console.log(gen.next());  // 5555, 6666

说明:生成器函数的执行流程:

  • function 后面会跟上符号*
  • 代码的执行可以被yeild控制
  • 生成器函数默认子在执行时,返回一个生成器对象
    • 要想执行函数内部的代码,需要生成器对象调用它的next()方法
    • 当遇到yeild时,就会中断执行

返回值 yield

通过yield返回结果

image-20241111150434975

传递参数 next()

函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?

  • 答案是可以的;

  • 我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值

  • 注意:也就是说我们是为本次的函数代码块执行提供了一个值

传递参数规则:

  • 第一个yield之前代码块中的参数name1是通过foo('next1')函数传递参数,并通过foo(name1)接收参数
  • 第二个yield之前代码块中的参数name2是在调用第二个next('next2')时传递参数,并通过const name2 = yield 'aaaa'中的name2接收参数
  • 之后的yield代码块中的参数按照第二个yield的参数传递规则依次传递。
  • 注意: 第一次调用next()时,不传递参数

image-20241111152253261

提前结束 return()

还有一个可以给生成器函数传递参数的方法是通过return函数

return传值后这个生成器函数就会立即结束,之后调用next不会继续生成值了;

image-20241111154038082

抛出异常 throw()

除了给生成器函数内部传递参数之外,也可以向生成器函数内部抛出异常

1、通过 generator.throw(new Error('error message')) 向生成器函数内部抛出异常

image-20241111154733928

2、在生成器函数内部捕获异常

注意: 在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;

image-20241111154950247

应用

生成器替代迭代器

我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器

▸ 利用生成器对之前的迭代器代码进行重构

1、之前的迭代器

image-20241111160212693

2、利用生成器重构之前的迭代器

image-20241111160303334


▸ 生成器函数,可以生成某个范围的值

image-20241111160701158

yield*

yield* 是一个委托表达式,用于将生成器的执行委托给另一个生成器可迭代对象。它会一次性执行另一个生成器,返回它的所有值。

语法:

js
yield* iterable
  • iterable:可以是任何具有迭代协议的对象,通常是另一个生成器、数组、字符串、Set、Map 等。

特性:

  • yield*yield 语句的一种语法糖。它会依次迭代这个可迭代对象,每次迭代其中的一个值。

  • yield* 只能存在于生成器函数中

  • yield* 语法非常适合处理递归生成器将多个生成器组合成一个生成器

用法:

▸ 基本用法:委托给另一个生成器

js
function* inner() {
  yield 1;
  yield 2;
}

function* outer() {
  yield* inner();  // 委托给 inner 生成器
  yield 3;
}

const gen = outer();
console.log(gen.next());  // { value: 1, done: false }
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: 3, done: false }
console.log(gen.next());  // { value: undefined, done: true }

▸ 一行代码重构迭代器:委托给数组

image-20241111161222048


▸ 使用 yield* 处理递归

js
// flatten 是一个递归生成器函数,使用 yield* 将内部的数组元素“平铺”到外部生成器中。
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* flatten(item);  // 递归地展开数组
    } else {
      yield item;
    }
  }
}

const gen = flatten([1, [2, [3, 4], 5], 6]);

console.log(gen.next());  // { value: 1, done: false }
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: 3, done: false }
console.log(gen.next());  // { value: 4, done: false }
console.log(gen.next());  // { value: 5, done: false }
console.log(gen.next());  // { value: 6, done: false }
console.log(gen.next());  // { value: undefined, done: true }

▸ 重构自定义类的迭代

在之前的 自定义类的迭代 中,我们也可以换成生成器:

注意: 此处*[Symbol.iterator]()表示生成器函数,yield*只能存在于生成器函数中。

image-20241111163245241

对生成器的操作

既然生成器是一个迭代器,那么我们可以对其进行如下的操作:

image-20230620144954281

异步处理@

学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。

异步处理方案:

  • 方案一:回调嵌套,会造成回调地狱
  • 方案二:Promise链式调用
  • 方案三:Generator方案
  • 方案四:async await方案(终极方案

案例需求:

  • 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;

  • 第二次的请求url依赖于第一次的结果;

  • 第三次的请求url依赖于第二次的结果;

  • 依次类推;

image-20241111173335351


▸ 方案一:回调嵌套,会造成回调地狱

image-20241111174007470


▸ 方案二:Promise链式调用

image-20241111174823793


▸ 方案三:Generator方案

▸ 方案四:async await方案(终极方案

Generator方案

但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?

image-20241111180048846

自动执行generator函数

目前我们的写法有两个问题

  • 第一,我们不能确定到底需要调用几层的Promise关系;

  • 第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?

所以,我们可以封装一个工具函数execGenerator自动执行生成器函数

image-20241113141944888

async await@

异步函数 async function

async关键字用于声明一个异步函数

  • async是asynchronous单词的缩写,异步、非同步;

  • sync是synchronous单词的缩写,同步、同时;

写法: async异步函数可以有很多中写法

image-20230809181802296

image-20230809173956879

特性:

▸ 异步函数默认情况下和普通函数一样

image-20230809181618309


▸ 异步函数返回的是一个Promise

image-20230810170218398

异步函数返回值

异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行

异步函数有返回值时,和普通函数会有区别

  • 情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到Promise.resolve中

  • 情况二:如果我们的异步函数的返回值是Promise,状态由会由Promise决定;

  • 情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定;


▸ 示例:返回一个普通的值

image-20230810170654019


▸ 示例:返回一个Promise

image-20230810170916861


▸ 示例:返回一个thenable

then方法中也可以有timeout()延迟方法

image-20230810171039568

异步函数rejected

什么情况下异步函数的结果是rejected?

  • 情况一:在异步函数中返回一个执行reject的Promise

  • 情况二:如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;

▸ 情况一:在异步函数中返回一个执行reject的Promise

image-20230810171640577


▸ 情况二:在异步函数中抛出了异常

image-20241113151347558

image-20230810172151632

await关键字

async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的。

image-20241113151921083

特点:

  • 通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise

  • await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数

表达式返回值类型:

  • 如果await后面是一个普通的值,那么会直接返回这个值;

  • 如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;

  • 如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的reject值;


▸ 基本用法:

image-20241113153016042


▸ await处理异步请求

image-20230810175240345


▸ 在异步函数中抛出异常的处理方式

image-20241113153753925

1、方式一:在异步函数后的catch中捕获异常

image-20241113153934160

2、方式二:在异步函数内部,通过try catch,捕获异常

image-20230810180056033


▸ await和async结合使用

await后面既可以跟随返回Promise的普通函数,也可以跟随一个异步函数

image-20241113155654367

image-20241113155637351