Skip to content

S16-00 专题-JS-手写

[TOC]

手写-call/aplly/bind

  • function.call()(thisArg,arg1?,arg2?,...),用于显式调用一个函数,并动态指定函数执行时的 this 值及参数列表。
  • function.apply()(thisArg,args?),用于显式调用一个函数,并动态指定函数执行时的 this 值及参数列表。
  • function.bind()(thisArg,arg1?,arg2?,...),用于创建一个新的函数,该函数在调用时会以指定的 this 值和预先提供的参数作为默认参数。

函数对象原型关系

函数 foo 对象的隐式原型 === Function 的显式原型

js
// 函数foo对象的隐式原型 === Function的显式原型
console.log(foo.__proto__ === Function.prototype) // true

console.log(Function.prototype.apply) // f apply()
console.log(Function.prototype.call) // f call()
console.log(Function.prototype.bind) // f bind()

console.log(Function.prototype.apply === foo.apply) // true

结论

  1. foo 对象中的某些属性和方法是来自 Function.prototype 的。
  2. Function.prototype 中添加的属性和方法,可以被所有的函数获取。

image-20250729224050108

在 Function 的原型中添加方法 bar

image-20250729224208694

手写 apply

前置知识

  1. 给函数对象添加方法:可以通过在 Function.prototype 上添加方法实现。

    image-20250730155358016


apply/call 使用示例

image-20250730155032244

基本实现

apply 实现思路

  • 将函数设置为目标对象的属性:mrthis.fn = this
  • 执行该函数:mrthis.fn(...args)
  • 删除添加的属性:delete mrthis.fn
  • 返回函数执行结果
js
function foo() {
  console.log('foo', this)
}

Function.prototype.mrapply = function (mrthis) {
  // 相当于 mrthis.fn = this
  Object.defineProperty(mrthis, 'fn', {
    configurable: true,
    value: this
  })
  // 隐式调用fn,可以让fn函数的this指向 mrthis
  mrthis.fn() // 相当于 this()
  // 删除多出来的临时函数fn
  delete mrthis.fn
}

foo.mrapply({ name: 'Tom' })

优化:this 类型处理

this 类型处理

  • 如果传入的 this 参数是一个 String 或者 Number 的类型,需要将其包裹成对象类型,才能在它上面添加属性

    image-20250730161529360

  • 如果传入的 this 参数是 undefined 或 null,this 指向 window

    image-20250730161553413

js
function foo(age, height) {
  console.log('foo', this, age, height)
}

Function.prototype.mrapply = function (mrthis) {
  // 当this不是对象时,需要用Object包裹
  mrthis = mrthis === null || mrthis === undefined ? window : Object(mrthis)

  // 相当于 mrthis.fn = this
  Object.defineProperty(mrthis, 'fn', {
    configurable: true,
    value: this
  })

  // 隐式调用fn,可以让fn函数的this指向 mrthis
  mrthis.fn()

  // 删除多出来的临时函数fn
  delete mrthis.fn
}

foo.mrapply({ name: 'Tom' })
foo.mrapply(null)
foo.mrapply(undefined)
foo.mrapply(true)
foo.mrapply(123)
foo.mrapply('aaaa')

优化:绑定参数

调用 mrapply 时,传递参数

image-20230224214829699

js
function foo(age, height) {
  console.log('foo', this, age, height)
}

Function.prototype.mrapply = function (mrthis, args) {
  // 当this不是对象时,需要用Object包裹
  mrthis = mrthis === null || mrthis === undefined ? window : Object(mrthis)

  // 相当于 mrthis.fn = this
  Object.defineProperty(mrthis, 'fn', {
    configurable: true,
    value: this
  })

  // 隐式调用fn,可以让fn函数的this指向 mrthis
  mrthis.fn(...args)

  // 删除多出来的临时函数fn
  delete mrthis.fn
}

foo.mrapply({ name: 'Tom' }, [18, 1.88])
foo.mrapply(null, [18, 1.88])
foo.mrapply(undefined, [18, 1.88])
foo.mrapply(true, [18, 1.88])
foo.mrapply(123, [18, 1.88])
foo.mrapply('aaaa', [18, 1.88])

手写 call

实现思路:与 call() 类似,但处理参数为数组形式。

js
function foo(age, height) {
  console.log('foo', this, age, height)
}

Function.prototype.mrcall = function (mrthis, ...args) {
  // 区别:apply:(mrthis, args)
  mrthis = mrthis === null || mrthis === undefined ? window : Object(mrthis)

  Object.defineProperty(mrthis, 'fn', {
    configurable: true,
    value: this
  })

  mrthis.fn(...args)

  delete mrthis.fn
}

foo.mrcall({ name: '张飞' }, 20, 1.77)

抽取封装公共函数

  1. 抽取封装的函数

    js
    /* 抽取封装的函数 */
    Function.prototype.mrexec = function (mrthis, args) {
      mrthis = mrthis === null || mrthis === undefined ? window : Object(mrthis)
    
      // mrthis.fn = this
      Object.defineProperty(mrthis, 'fn', {
        configurable: true,
        value: this
      })
    
      mrthis.fn(...args)
    
      delete mrthis.fn
    }
  2. 手写 apply

    js
    /* 手写apply */
    Function.prototype.mrapply = function (mrthis, args) {
      this.mrexec(mrthis, args)
    }
  3. 手写 call

    js
    /* 手写call */
    Function.prototype.mrcall = function (mrthis, ...args) {
      this.mrexec(mrthis, args)
    }
  4. 测试

    js
    // 测试
    function foo(age, height) {
      console.log('foo', this, age, height)
    }
    foo.mrapply({ name: 'Tom' }, [19, 1.66])
    foo.mrcall({ name: 'Jack' }, 22, 1.99)

手写 bind

bind 使用示例:和 apply/call 不同,bind 执行后是返回一个新的函数 newFoo。

image-20230225114537180

基本实现

思路:想办法实现如下:

js
// 伪代码
{ name: "why" }.foo(name, age)
js
/* 手写bind */
Function.prototype.mrbind = function (mrthis, ...args) {
  return (...moreArgs) => {
    mrthis = mrthis === null || mrthis === undefined ? window : Object(mrthis)

    Object.defineProperty(mrthis, 'fn', {
      configurable: true,
      value: this
    })

    // 合并2个方法中传递的参数
    const allArgs = [...args, ...moreArgs]

    mrthis.fn(...allArgs) // 相当于 this()

    delete mrthis.fn // 可以删除fn,因为每次调用newFoo,都会重新生成一个mrthis.fn
  }
}

// 测试
function foo(name, age, height, address) {
  console.log('foo', this, name, age, height, address)
}
const newFoo = foo.mrbind({ name: 'Jerry' }, '张飞', 45)
console.log(newFoo)
newFoo(1.88, '成都')
newFoo(1.88, '成都')

手写-防抖/节流

概述

防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中

  • 而 JavaScript 是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。

  • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题

但是很多前端开发者面对这两个功能,有点摸不着头脑:

  • 某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);

  • 某些开发者可以区分,但是不知道如何应用;

  • 某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;

接下来我们会一起来学习防抖和节流函数:

  • 我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;

  • 并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;

防抖

防抖(Debounce):是一种前端性能优化技术,用于限制高频触发事件的回调函数执行次数。其核心思想是:事件触发后,等待一段时间再执行回调。若在等待期间事件再次触发,则重新计时,直到等待期结束后才真正执行一次回调。


类比理解(电梯场景)

想象你在电梯门口:

  • 有人进电梯(触发事件)→ 电梯门开始关闭(开始计时)。
  • 若在关门期间又有人进来(再次触发) → 电梯门重新打开(重置计时)。
  • 直到连续 N 秒无人进入(等待期结束) → 电梯门关闭并运行(执行一次回调)。

图示理解

我们用一副图来理解一下它的过程:

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;

  • 当事件密集触发时,函数的触发会被频繁的推迟

  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数

image-20230620152349381

应用场景:

防抖的应用场景很多:

  • 搜索联想oninput,输入框中频繁的输入内容,搜索或者提交信息;

  • 频繁点击按钮onclick,频繁的点击按钮,触发某个事件;

  • 浏览器滚动事件onscroll,监听浏览器滚动事件,完成某些特定操作;

  • 浏览器缩放事件onresize,用户缩放浏览器的 resize 事件;


示例:搜索联想

我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容

image-20230620152423458

比如想要搜索一个 MacBook:

  • 当我输入 m 时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;

  • 当继续输入 ma 时,再次发送网络请求;

  • 那么 macbook 一共需要发送 7 次网络请求;

  • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;

但是我们需要这么多次的网络请求吗?

  • 不需要,正确的做法应该是在合适的情况下再发送网络请求;

  • 比如如果用户快速的输入一个 macbook,那么只是发送一次网络请求;

  • 比如如果用户是输入一个 m 想了一会儿,这个时候 m 确实应该发送一次网络请求;

  • 也就是我们应该监听用户在某个时间,比如 500ms 内,没有再次触发时间时,再发送网络请求;

这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数

我们通过一个搜索框来延迟防抖函数的实现过程:

  • 监听 input 的输入,通过打印模拟网络请求

测试发现快速输入一个 macbook 共发送了 7 次请求,显示我们需要对它进行防抖操作:

image-20230620152534787

image-20230620152544034


生活中防抖的例子:

比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。

如果在五分钟的时间内,没有同学问我问题,那么我就下课了;

  • 在此期间,a 同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;

  • 如果我等待超过了 5 分钟,就点击了下课(才真正执行这个时间);

节流

节流(throttle)

我们用一副图来理解一下节流的过程

  • 当事件触发时,会执行这个事件的响应函数;

  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数

  • 不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;

image-20230620152442909

应用场景:

  • 页面滚动事件:监听页面的滚动事件;

  • 鼠标移动事件

  • 频繁点击事件:用户频繁点击按钮操作;

  • 游戏某些设计:游戏中的一些设计,如发射子弹;

很多人都玩过类似于飞机大战的游戏

在飞机大战的游戏中,我们按下空格会发射一个子弹:

  • 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;

  • 比如 1 秒钟只能发射一次,即使用户在这 1 秒钟按下了 10 次,子弹会保持发射一颗的频率来发射;

  • 但是事件是触发了 10 次的,响应的函数只触发了一次;

image-20230620152511039


生活中节流的例子:

比如说有一天我上完课,我说大家有什么问题来问我,但是在一个 5 分钟之内,不管有多少同学来问问题,我只会解答一个问题;

如果在解答完一个问题后,5 分钟之后还没有同学问问题,那么就下课;

手写防抖

我们按照如下思路来实现:

  • 防抖基本功能实现:可以实现防抖效果
  • 优化一:优化参数和 this 指向
  • 优化二:优化取消操作(增加取消功能)
  • 优化三:优化立即执行效果(第一次立即执行)
  • 优化四:优化返回值

基本实现

image-20230913170819191

优化:参数和 this 绑定

this 指向

image-20230913171801374

参数

image-20230913172010545

image-20230913172141908

优化:取消功能

image-20230913173224970

image-20230913173251166

优化:第一次立即执行

  • immediate:控制否时启用立即执行功能
  • isInvoke:控制函数是否已经立即执行一次了

image-20230913175323462

image-20230913175525645

优化:返回值

image-20230914120206319

image-20230914120006631

image-20230914120016985

手写节流

我们按照如下思路来实现:

  • 节流函数的基本实现:可以实现节流效果
  • 优化一:绑定 this 和参数
  • 优化二:控制立即执行,节流最后一次也可以执行
  • 优化三:优化添加取消功能
  • 优化四:优化返回值问题

基本实现

image-20230914145011970

image-20230914144933945

优化:绑定 this 和参数

image-20230914145650570

image-20230914145254694

优化:控制立即执行

image-20230914150800082

image-20230914145957177

优化:控制执行最后一次

思路一: 给每次点击时添加一个定时器,延迟时间设为 waitTime,当再次点击时取消上次的定时器,重新添加一个。

思路二: 在每个执行 fn 函数的节点,添加一个延迟时间为 waitTime 的定时器,当用户在 fn 函数执行节点的时间上也点击了一次就取消该定时器(使用中

image-20230914161141810

优化:取消功能

image-20230914173427834

image-20230914173431437

优化:返回值

image-20230914174045131

image-20230914173857111

手写-深拷贝函数

浅拷贝/深拷贝

对象相互赋值

前面我们已经学习了对象相互赋值的一些关系,分别包括:

js
const info = {
  name: 'tom',
  age: 18,
  friend: {
    name: 'jack'
  }
}
  • 引用赋值:指向同一个对象,相互之间会影响;

    js
    const obj1 = info
    
    // 特性:obj1 和 info 指向同一个对象

    image-20250802224359408

  • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;

    js
    const obj2 = { ...info } // 方式一:
    const obj3 = Object.assign({}, info) // 方式二:
    
    // 特性1:修改 obj2.name,原对象 info.name 的值依然是 'tom'
    obj2.name = '张飞'
    console.log(info.name) // 'tom'
    
    // 特性2:修改 obj2.friend.name,原对象 info.friend.name 的值会随之改变成 '曹操'
    obj2.friend.name = '曹操'
    console.log(info.friend.name) // '曹操'

    image-20250802225130827

  • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;

    image-20250802225249480


深拷贝实现方式

  • JSON.parse

    js
    const obj = JSON.parse(JSON.stringify(info))
  • 第三方库:underscore、lodash

  • 自己实现


JSON.parse()的缺点

前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse()

  • 这种深拷贝的方式其实对于函数Symbol等是无法处理的;

    image-20250724174537845

  • 并且如果存在对象的循环引用,也会报错的;

    image-20250724175206921

手写深拷贝

  • 1.自定义深拷贝的基本功能;

  • 2.对 Symbol 的 key 进行处理;

  • 3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;

  • 4.对循环引用的处理;

工具函数:判断对象

实现思路:通过 typeof 判断,并对 typeof 返回的结果进行分析,找出是对象的类型返回。

特殊值null不算object类型;functionobject类型。

image-20250724180744248

基本实现

image-20250724181208147

image-20230915091526589

优化:区分数组和对象

实现思路:通过 Array.isArray() 区分是数组还是对象。

image-20250724210654225

image-20250724213755166

优化:其他类型-Set

问题:当对象中有 Set 类型时,无法拷贝。

分析:这是因为 deepCopy() 内部是通过对目标对象执行 for...in 操作进行遍历的,而 Set 通过 for...in 操作返回的是空值。

image-20230915095300660

image-20250724214745653

优化:其他类型-Map

思路和 Set 一致

优化:其他类型-Function

function: 不需要深拷贝

image-20250724220957111

image-20250724220911458

优化:其他类型-Symbol 为值

问题:当对象中有 Symbol 类型的值时,会直接返回原始的 Symbol 值,此处的新值和旧值指向同一个 Symbol。

image-20230915102436519

image-20230915102117655

优化:其他类型-Symbol 为 key

问题:Symbol 作为 key 时,无法通过 for...in 遍历出来。

解决:必须对 Symbol 类型的 key 单独使用 Object.getOwnPropertySymbols(),再通过 for...of 遍历。

image-20250724222319587

image-20230915102523200

优化:处理循环引用

循环引用问题:当原始对象出现循环引用时,调用 deepCopy() 会出现无限递归,最终报错:

image-20250724222814561

image-20250724222831326

方案一:将每次新创建的对象保存到 Map 中,每次遍历前判断之前是否已经保存过了该对象。

问题:需要在 deeCopy 外部定义一个 map,并且每次拷贝完成后 map 依然会形成对对象的强引用,没有销毁。

image-20250724224145263

方案二(推荐):使用 WeakMap 替代 Map;将 map 放入参数中并设置一个默认值new WeakMap()

image-20230915105909339

手写-事件总线

事件总线

  • mitt()(all?),用于创建一个事件总线实例,支持事件的监听、触发和移除。
  • emitter.on()(type,handler),用于 监听指定类型的事件,当该事件被触发时,执行绑定的处理函数。支持监听具体事件类型或使用通配符 * 监听所有事件。
  • emitter.off()(type,handler?),用于 移除指定事件类型的监听器,支持移除单个处理函数或清空某事件类型的所有监听器,避免内存泄。
  • emitter.emit()(type,data?),用于 触发指定类型的事件,并传递相关数据给所有监听该事件的处理器。
  • emitter.all{eventName: Handler[]},用于存储所有已注册的事件类型及其对应的事件处理器列表,用于调试或高级操作。

自定义事件总线属于一种观察者模式,其中包括三个角色:

  • 发布者(Publisher):发出事件(Event);

  • 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);

  • 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;

当然我们可以选择一些第三方库

  • Vue2 默认是带有事件总线的功能;

  • Vue3 中推荐一些第三方库,比如mitt

当然我们也可以实现自己的事件总线:

  • 事件的监听方法 on;

  • 事件的发射方法 emit;

  • 事件的取消监听 off;

手写事件总线

基本实现

image-20250726083708584

image-20250726083822511

优化:绑定参数

image-20230915114132235

image-20250726083944393

优化:移除监听

实现思路

  • 遍历 eventMap中事件名对应的事件函数数组,找到需要删除的事件函数,将其删除。
  • 如果事件名对应的事件函数数组已经为空,则删除该事件名属性。

image-20250726084606706

image-20250726084743977

完整代码

image-20230915114603806

image-20230915114736411

手写-Promise

Promise 结构设计

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

Promise 三种状态

实现思路

  1. Promise()会传入一个 executor 回调函数,它在 constructor 中会被立即调用
  2. 调用 executor()回调函数时,会接收 resolve 和 reject 2 个回调函数
  3. 当调用 resolve 或 reject 时,会修改 promise 的状态(pending -> fulfilled 或 pending -> rejected)

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

image-20250812161327312

分析: 这是由于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)
//   }
// )

手写-jQuery

轮播图【

任务: 分别使用 JS 原生、Vue、React 手写一个轮播图

遍历目录下所有文件

  • fs.readdirSync()(path, options?),用于读取指定目录中的所有文件和子目录,并返回一个包含文件名的数组。

    • pathstring | Buffer | URL,想要读取的目录的路径。

    • options?'utf8' | Object默认:Buffer,它有两种不同的形式:

      • 'utf8':以 UTF-8 编码返回文件名,
      • Object{encoding?, withFileTypes?, recursive?},包含以下选项的对象
        • encoding?string默认:'utf8',如果是 utf8,返回字符串名字;如果不设或其他编码格式,返回 Buffer 格式的名字。
        • withFileTypes?boolean默认:false,是否返回包含fs.Dirent对象的名字。
        • recursive?boolean默认:false,是否递归遍历目录及子目录。
    • 返回:

    • filesstring[] | Dirent[],返回包含文件的数组。

      • 如果withFileTypes:false返回string[]包含文件名的数组

        如果withFileTypes:false返回Dirent[]对象数组,每个 Dirent 对象代表目录中的一个文件或子目录。

    • js
      const fs = require('fs')
      
      try {
        const files = fs.readdirSync('/path/to/directory')
        console.log(files) // 输出文件和子目录的名字(数组)
      } catch (err) {
        console.error('读取目录失败:', err)
      }
  • dirent.isDirectory()(),判断是否是一个目录。需要设置withFileTypes: true返回一个 Dirent 对象才能判断。

    • 返回: boolean,如果为 true,表示是一个目录;如果为 false,表示不是一个目录。

    • js
      const fs = require('fs')
      
      // 读取目录,使用 withFileTypes: true 来获取 Dirent 对象
      const files = fs.readdirSync('/path/to/directory', { withFileTypes: true })
      
      files.forEach((file) => {
        if (file.isDirectory()) {
          console.log(`${file.name} 是一个目录`)
        } else {
          console.log(`${file.name} 不是一个目录`)
        }
      })

代码实现:

js
// 环境:node
// 运行:node ./index.js

const path = require('node:path')
const fs = require('node:fs')

/**
 * 遍历目录下所有文件
 * @param {string} dir 需要遍历的目录
 * @param {object} [options] 遍历选项
 * @param {boolean} [options.recursive = true] 是否遍历子目录,默认值true
 * @param {boolean} [options.withDirectory] 是否包含目录,默认值true。
 * @returns {{type, filename}[]} allFiles 返回所有文件,包括目录和子目录下的文件
 * @example
 * // 基本使用:递归遍历目录,并包含目录
 * scanFolder('path/to/dir')
 *
 * @example
 * // 其他用法:不遍历子目录,不包含目录
 * scanFolder('path/to/dir', { recursive: false, withDirectory: false })
 */
function scanFolder(dir, { recursive = true, withDirectory = true } = {}) {
  const allFiles = []
  const rootFolder = dir
  function scan(folder) {
    // 核心API:fs.readdirSync()
    const files = fs.readdirSync(folder, { withFileTypes: true })
    files.forEach((file) => {
      const name = path.resolve(file.path, file.name).replace(rootFolder + '\\', '')
      if (file.isDirectory()) {
        if (withDirectory) {
          allFiles.push({ type: 'dir', filename: name })
        }
        if (recursive) {
          scan(path.resolve(folder, file.name))
        }
      } else {
        allFiles.push({ type: 'file', filename: name })
      }
    })
  }
  scan(dir)
  return allFiles
}

测试:

js
// 返回结果:
// [
//   { type: 'dir', filename: 'd01-scan-folder' },
//   { type: 'file', filename: 'd01-scan-folder\\index.js' },
//   { type: 'dir', filename: 'demo' },
//   { type: 'file', filename: 'demo\\web.js' },
//   { type: 'file', filename: 'test.js' }
// ]
const dir = path.resolve(__dirname, '../') // D:\2024\Learn\Web\S21-Demo
console.log(scanFolder(dir))

// console.log(scanFolder(dir, { recursive: false, withDirectory: false }))

image-20241114170132392