手写 JS 面试题整理

60 阅读20分钟

宏任务/微任务实践

  • 题目:
    console.log('script start')
    async function async1() {
      await async2()
      console.log('async1 end')
    }
    async function async2() {
      console.log('async2 end')
    }
    async1()
    setTimeout(function() {
      console.log('setTimeout')
    }, 0)
    new Promise(resolve => {
      console.log('Promise')
      resolve()
    }).then(function() {
      console.log('promise1')
    }).then(function() {
      console.log('promise2')
    })
    console.log('script end')
    
  • 答案:script start, async2 end, Promise, script end, async1 end, promise1, promise2, setTimeout

手写函数防抖和函数节流

  • 函数防抖 接到任务不会立即执行,会等一段固定的时间,如果又接到了新任务就会重新等那段时间,如果这段时间没有新的任务就执行已经接到的任务。使用场景:
    • 输入验证:在用户输入时,例如搜索框,可能需要实时校验输入内容的合法性。使用防抖可以避免在用户输入过程中频繁触发验证,而是在用户停止输入一段时间后再进行验证。
    • 按键事件:在实现实时搜索或自动完成功能时,用户的每次按键都会触发事件。使用防抖可以减少事件处理的次数,提高性能。
    • API 请求:在进行实时数据更新或轮询时,使用防抖可以减少不必要的API请求,只在用户停止操作一段时间后才发送请求。
    • 表单提交:在表单提交前,可能需要进行多次验证。使用防抖可以确保在用户完成输入后,只进行一次验证。
      function debounce(fn, delay) {
        let timerId = null
        return function() {
          const context = this
          if(timerId) { window.clearTimeout(timerId) }
          timerId = setTimeout(()=>{
            fn.apply(context, arguments)
            timerId = null
          }, delay)
        }
      }
    
  • 函数节流 执行第一次后的一段时间内不会执行第二次,类似于冷却时间。使用场景:
    • 窗口调整:当浏览器窗口大小变化时,可能会触发一系列的事件,如 resize。使用节流可以限制这些事件的处理频率,避免在窗口尺寸连续变化时执行过多的操作。
    • 滚动事件:滚动条滚动时会频繁触发 scroll 事件。使用节流可以限制处理这些事件的频率,例如在滚动停止一段时间后再执行某些操作。
    • 无限滚动:在实现无限滚动加载内容的功能时,使用节流可以控制加载新内容的频率,避免因为滚动过快而导致的频繁请求。
    • 鼠标移动:在实现鼠标悬停效果或绘图功能时,鼠标移动会触发 mousemove 事件。使用节流可以限制事件处理的频率,减少计算量。
    • 动画帧:在某些动画效果中,可能需要根据每一帧的变化来更新状态。使用节流可以确保动画的平滑性,避免因为帧率过高而导致的性能问题。
    • DOM 操作:在进行复杂的 DOM 操作时,尤其是涉及到重绘或回流的,使用节流可以减少操作的频率,提高性能。
      function throttle(fn, delay) {
        let canUse = true
        return function() {
          if(canUse) {
            fn.apply(this, arguments)
            canUse = false
            setTimeout(()=>canUse = true, delay)
          }
        }
      }
    

深拷贝

function clone(target, map = new Map()) {
  if (typeof target !== 'object' || target === null) { return target }
  if (target instanceof Date) {
    return new Date(target)
  }
  if (target instanceof RegExp) {
    return new RegExp(target)
  }
  let cloneTarget = Array.isArray(target) ? [] : {}
  if (map.get(target)) {
    return map.get(target)
  }
  map.set(target, cloneTarget)
  for (const key in target) {
    cloneTarget[key] = clone(target[key], map)
  }
  return cloneTarget
}

手写 Promise

  • 基础版
class Promise2 {
  quene1 = []  //成功队列,容纳成功后的函数
  quene2 = []  //失败队列
  constructor(fn)  //new Promise()之后调用
    const resolve = (data) => {
      setTimeout(() => {  //resolve()写在then()之前,但需要在then()执行之后,否则队列为空
        for(let i=0; i<this.quene1.length; i++) {
          this.quene1[i](data)
        }
      })
    }
    const reject = (reason) => {
      setTimeout(() => {
        for(let i=0; i<this.quene2.length; i++) {
          this.quene2[i](reason)
        }
      })
    }
    fn(resolve, reject)
  }
  then(success, error) {
    this.quene1.push(success)  //将成功之后的函数加入成功队列
    this.quene2.push(error)  //将失败之后的函数加入失败队列
    return this  //返回自身,就可以再次调用自身的api,满足链式调用
  }
}

p1 = new Promise2((resolve, reject) => {
  console.log('hi');
  resolve()
})
p1.then(() => {console.log('成功1')}, () => {console.log('失败1')})
  .then(() => {console.log('成功2')}, () => {console.log('失败2')})

//此版本未实现失败承包制

用 class 如何实现继承?不用 class 又如何实现?

  • 使用 class
      class Animal{
        constructor(color){
          this.color = color
        }
        move(){}
      }
      class Dog extends Animal{
        constructor(color, name){
          super(color)
          this.name = name
        }
        say(){}
      }
    
  • 不使用 class
      function Animal(color){
        this.color = color
      }
      Animal.prototype.move = function(){} // 动物可以动
      function Dog(color, name){
        Animal.call(this, color) // 或者 Animal.apply(this, arguments)
        this.name = name
      }
      // 下面三行实现 Dog.prototype.__proto__ = Animal.prototype
      function temp(){}
      temp.prototype = Animal.prototype
      Dog.prototype = new temp()
    
      Dog.prototype.constuctor = Dog // 这行看不懂就算了,面试官也不问
      Dog.prototype.say = function(){ console.log('汪')}
    
      var dog = new Dog('黄色','阿黄')
    

实现数组去重

  • 使用 Set [...new Set(arr)]Array.from(new Set(arr))
  • 使用reduce
    function unique(arr) {
      return arr.reduce((newArr, current) => {
        if (!newArr.includes(current)) {
          newArr.push(current)
        }
        return newArr
      }, [])
    }
    
  • 定义一个新数组,并存放原数组的第一个元素,然后将元素组一一和新数组的元素对比,若不同则存放在新数组中
      function unique(arr) {
        let newArr = [arr[0]]
        for (let i = 1; i < arr.length; i++) {
          let repeat = false
          for (let j = 0; j < newArr.length; j++) {
            if (arr[i] === newArr[j]) {
              repeat = true
              break
            }else{
    
            }
          }
          if (!repeat) {
            newArr.push(arr[i])
          }
        }
        return newArr
      }
    
  • 先将原数组排序,在与相邻的进行比较,如果不同则存入新数组
      function unique2(arr) {
        var formArr = arr.sort()
        var newArr=[formArr[0]]
        for (let i = 1; i < formArr.length; i++) {
          if (formArr[i]!==formArr[i-1]) {
            newArr.push(formArr[i])
          }
        }
        return newArr
      }
    
  • 利用对象属性存在的特性,如果没有该属性则存入新数组
      function unique3(arr) {
        var obj={}
        var newArr=[]
        for (let i = 0; i < arr.length; i++) {
          if (!obj[arr[i]]) {
            obj[arr[i]] = 1
            newArr.push(arr[i])
          }   
        }
        return newArr
      }
    
  • 利用数组的indexOf下标属性来查询
      function unique4(arr) {
        var newArr = []
        for (var i = 0; i < arr.length; i++) {
          if (newArr.indexOf(arr[i])===-1) {
            newArr.push(arr[i])
          }
        }
        return newArr
      }
    
  • 利用数组原型对象上的forEachincludes方法
      function unique7(arr) {
        let newArr = []
        arr.forEach(item => {
          return newArr.includes(item) ? '' : newArr.push(item)
        })
        return newArr
      }
    

实现一个 new

function _new(func, ...args) {
  let obj = Object.create(func.prototype) // 原型
  let res = func.apply(obj, args) // 初始化对象属性 
  return (res instanceof Object) ? res : obj // 返回值
}

实现千分位

  • 使用toLocaleString
    let num = 1234567.89
    let formattedNum = num.toLocaleString()
    
  • 手动实现
    function formatNumber(num) {
      const numStr = num.toString()
      let result = ''
      let counter = 0
      for (let i=numStr.length-1; i>=0; i--) {
        counter++
        result = numStr[i] + result
        if (!(counter % 3) && i !== 0) {
          result = ',' + result
        }
      }
      return result
    }
    

实现函数柯里化

function curry(fn) {
  if (typeof fn !== 'function') {
    throw new Error('curry() requires a function')
  }
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2))
      }
    }
  }
}

在这个例子中,curry 函数接受一个函数 fn 作为参数,并返回一个新的函数 curried。curried 函数会检查是否已经提供了足够的参数来调用原始函数 fn。如果是,则直接调用 fn 并返回结果。否则,它返回一个新的函数,该函数接受剩余的参数,并将它们与已经收集的参数合并,然后递归调用 curried。这个过程会一直持续到收集到足够的参数为止。

注意,这个函数柯里化的实现假设了原始函数 fn 的参数数量是固定的。如果 fn 接受可变数量的参数,那么这个实现可能需要进行相应的调整。

实现数组扁平化

  • 使用flat方法
    let nestedArray = [1, [2, [3, [4]], 5]]
    console.log(nestedArray.flat(Infinity)) // 输出 [1, 2, 3, 4, 5]
    
  • 使用扩展运算符
    function flattenArray(arr) {
      while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr)
      }
      return arr
    }
    
  • 使用reduce方法
    function flattenArray(arr) {
      return arr.reduce((acc, val) => Array.isArray(val) ? [...acc, ...flattenArray(val)] : [...acc, val], [])
    }
    
  • 使用flat方法
    function flattenArray(arr) {
      let result = []
      for (let i=0; i<arr.length; i++) {}
      if (Array.isArray(arr[i])) {
        result = result.concat(flattenArray(arr[i]))
      } else {
        result.push(arr[i])
      }
      return result
    }
    

实现数组元素的求和

  • 使用Array.prototype.reduce方法
    function sumArray(arr) {
      return arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0)
    }
    
  • 使用for循环
    function sumArray(arr) {
      let sum = 0
      for (let i=0; i<arr.length; i++) {
        sum += arr[i]
      }
      return sum
    }
    

将 JS 对象转换为树形结构

function buildTree(items, parentId = null) {
  let tree = []
  for (let i in items) {
    if (items[i].parentId == parentId) {
      const children = buildTree(items, items[i].id)
      if (children.length) {
        items[i].children = children
      }
      tree.push(items[i])
    }
  }
  return tree
}

上面的 buildTree 函数会递归遍历 items 数组,查找所有具有指定 parentId 的项。对于找到的每个项,它又会递归地查找所有以该项的 id 为 parentId 的子项,并将这些子项作为 children 数组附加到该项上。最终,所有顶级项(即parentId为null的项)将被收集到 tree 数组中并返回。

实现发布-订阅模式

class PubSub {
  constructor() {
    this.subscribers = {}
  }
  // 订阅事件
  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = []
    }
    this.subscribers[event].push(callback)
  }
  // 取消订阅事件
  unsubscribe(event, callback) {
    if (this.subscribers[event]) {
      this.subscribers[event] = this.subscribers[event].filter(subCallback => subCallback !== callback)
    }
  }
  // 发布事件
  publish(event, data) {
    if (this.subscribers[event]) {
      this.subscribers[event].forEach(callback => callback(data))
    }
  }
}  

在这个例子中,创建了一个 PubSub 类,它有三个方法:subscribe、unsubscribe 和 publish。subscribe 方法用于订阅事件,它将回调函数存储在一个以事件名为键的对象中。unsubscribe 方法用于取消订阅事件,它从存储的回调函数中移除指定的回调函数。publish 方法用于发布事件,它遍历所有订阅了该事件的回调函数,并调用它们。

字符串中出现的不重复字符的最长长度

  • 第一种
function lengthOfLongestSubstring(s) {
  let start = 0 // 窗口起始位置
  let maxLength = 0 // 最长不重复子串的长度
  let seen = new Set() // 用于存储窗口内的字符
  for (let end=0; end<s.length; end++) {
    // 如果当前字符已经在窗口内,则移动窗口的起始位置
    while (seen.has(s[end])) {
      seen.delete(s[start])
      start++
    }
    // 将当前字符添加到窗口内
    seen.add(s[end])
    // 更新最长不重复子串的长度
    maxLength = Math.max(maxLength, end - start + 1)
  }
  return maxLength
}

在这个函数中,使用了一个 Set 数据结构 seen 来存储当前窗口内的字符。当遇到一个新的字符时,检查它是否已经在 seen 集合中。如果在,则说明这个字符在窗口内重复了,需要移动窗口的起始位置 start,并从 seen 中移除相应的字符,直到这个重复字符不再出现在窗口中。然后,将新字符添加到 seen 集合中,并更新最长不重复子串的长度。最后,返回最长不重复子串的长度 maxLength。

  • 第二种
function lengthOfLongestSubstring(s) {
  // 获取输入字符串的长度
  let n = s.length
  // 用于存储最长子串的长度,初始值为 0
  let maxLength = 0
  // 创建一个 Map 数据结构,用于存储字符及其在字符串中的位置
  let map = new Map()
  // start 指针表示当前不重复子串的起始位置
  let start = 0
  // 使用 end 指针遍历整个字符串
  for (let end = 0; end < n; end++) {
    // 检查当前字符是否已经在 map 中存在(即是否重复出现)
    if (map.has(s[end])) {
      // 如果当前字符重复出现,更新 start 指针的位置
      // 取 start 和重复字符上一次出现位置的下一个位置中的较大值
      start = Math.max(start, map.get(s[end]) + 1)
    }
    // 将当前字符及其位置存入 map
    map.set(s[end], end)
    // 更新最长子串的长度
    // 计算当前不重复子串的长度(end - start + 1),并与 maxLength 比较取较大值
    maxLength = Math.max(maxLength, end - start + 1)
  }
  // 返回最长子串的长度
  return maxLength
}

交换两个变量的值,不使用临时变量

// 使用解构赋值
let a = 5
let b = 10
[a, b] = [b, a]

实现一个类型判断函数

  1. 判断 null
  2. 判断基础类型
  3. 使用 Object.prototype.toString.call(target) 来判断饮用类型

**注意:**一定是使用 call 来调用,不然是判断的 Object.prototype 的类型。之所以要先判断是否为基本类型,是因为虽然 Object.prototype.toString.call() 能判断出某值是 number/string/boolean,但是其实在包装的时候是把他们先转成了对象,然后再判断类型的。但是JS中包装类型和原始类型还是有差别的,因为对一个包装类型来说,typeof 的值是 object。

代码:

function getType(target) { 
  //先处理最特殊的Null
  if(target === null) {
    return 'null'
  }
  //判断是不是基础类型
  const typeOfT = typeof target
  if(typeOfT !== 'object') {
    return typeOfT
  }
  //肯定是引用类型了
  const template = {
    "[object Object]": "object",
    "[object Array]" : "array",
    // 一些包装类型
    "[object String]": "object - string",
    "[object Number]": "object - number",
    "[object Boolean]": "object - boolean"
  }
  const typeStr = Object.prototype.toString.call(target)
  return template[typeStr]
}

手写AJAX

var request = new XMLHttpRequest()
request.open('GET', '/a/b/c?name=ff', true);
request.onreadystatechange = function () {
  if(request.readyState === 4 && request.status === 200) {
    console.log(request.responseText);
  }
};
request.send();

算法

实现斐波那契数列:斐波那契数列是一个经典的数学序列,第一项等于0,第二项等于1,其特点是从第三项开始,每一项都等于前两项之和。

// 递归实现
function fibonacci1(n) {
  if (n === 0) { return 0 }
  if (n === 1) { return 1 }
  return fibonacci1(n - 1) + fibonacci1(n - 2)
}
// 时间复杂度:O(2的n次方);空间复杂度:O(n)

// 记忆化递归实现
function fibonacci2() {
  const memo = {}
  function fibonacci(n) {
    if (n === 0) { return 0 }
    if (n === 1) { return 1 }
    if (memo[n]) { return memo[n] }
    memo[n] = fibonacci(n - 1) + fibonacci(n - 2)
    return memo[n]
  }
  return fibonacci
}
// 时间复杂度:O(n);空间复杂度:O(n)

// 迭代实现
function fibonacci3(n) {
  if (n === 0) { return 0 }
  if (n === 1) { return 1 }

  let a = 0
  let b = 1
  for (let i = 2; i <= n; i++) {
    let temp = b
    b = a + b
    a = temp
  }
  return b
}
// 时间复杂度:O(n);空间复杂度:O(1)

写一个函数,它有两个参数,第一个是 URL,第二个是参数名,然后能够解析这个 URL 里面的参数,根据参数名获取到对应的参数值。

function getParamValue(url, paramName) {
  const urlObject = new URL(url);
  const searchParams = urlObject.searchParams;
  return searchParams.get(paramName);
}
  • 首先,我们使用 URL 构造函数将传入的 URL 字符串转换为一个 URL 对象。这样方便我们操作 URL 中的查询参数部分。
  • 然后,通过 searchParams 属性获取到查询参数对象。
  • 最后,使用 get 方法,传入我们要查找的参数名 paramName,这个方法会返回对应的参数值,如果参数不存在则返回 null。

实现一个函数在给定字符串中搜索子字符串,并返回其个数。接受三个参数,第一个是要搜索完整的字符串,第二个是要搜索字符串。第三个参数布尔值表示重叠的结果,true 就计算在内,false 就不计算重叠的部分。

function countSubstrings(fullString, subString, overlapping) {
  let count = 0;
  for (let i = 0; i < fullString.length; i++) {
    if (fullString.substring(i).startsWith(subString)) {
      count++;
      if (!overlapping) {
        i += subString.length - 1;
      }
    }
  }
  return count;
}

给定一个混合液体玻璃杯的二维数组,使液体根据其密度在玻璃杯中排序,低密度上浮玻璃杯的宽度不会变。

function sortLiquids(glass) {
  const flattened = glass.flat();
  const sortedFlattened = flattened.sort((a, b) => {
    let densityA = 0, densityB = 0;
    for (let item of a) {
      densityA += item === 0? 1 : item === 1? 2 : 3;
    }
    for (let item of b) {
      densityB += item === 0? 1 : item === 1? 2 : 3;
    }
    return densityA - densityB;
  });
  const result = [];
  for (let i = 0; i < glass.length; i++) {
    result.push(sortedFlattened.splice(0, glass[i].length));
  }
  return result;
}

这个函数的目的是对给定的玻璃杯(二维数组)中的液体进行排序。其中,液体用数字 0、1、2 表示不同的种类,假设它们分别对应不同的密度,函数将根据这些数字所代表的密度总和对每个杯中的液体行进行排序。

  1. const flattened = glass.flat();
  • 将二维数组 glass 扁平化,即将多维数组转换为一维数组,方便后续的排序操作。
  1. const sortedFlattened = flattened.sort((a, b) => {... });
  • 使用数组的 sort 方法对扁平化后的数组进行排序。
  • 在比较函数中,遍历 a 和 b(分别代表两个要比较的 “杯中的液体行”),计算每个行中液体的密度总和。
  • 根据密度总和的差值进行排序,返回小于 0 的值表示 a 应该排在 b 之前,大于 0 的值表示 b 应该排在 a 之前,等于 0 表示它们的顺序不变。
  1. const result = []; 和 for 循环
  • 创建一个空数组 result 用于存储排序后的结果。
  • 遍历原始二维数组的行数,每次从排序后的扁平化数组中截取与该行长度相等的部分,并将其作为新的一行添加到 result 数组中。

实现一个函数来计算背包能携带的最大物品价值。函数参数三个数组组成,第一个是得分,第二个是权重。两个参数总是等长的有效数组,因此不用验证输入。第三个参数是背包不能超过的最大重量。

function knapsackValue(scores, weights, maxWeight) {
  const n = scores.length;
  const dp = Array.from({ length: n + 1 }, () => Array(maxWeight + 1).fill(0));

  for (let i = 1; i <= n; i++) {
    for (let w = 0; w <= maxWeight; w++) {
      if (weights[i - 1] <= w) {
        dp[i][w] = Math.max(dp[i - 1][w], scores[i - 1] + dp[i - 1][w - weights[i - 1]]);
      } else {
        dp[i][w] = dp[i - 1][w];
      }
    }
  }

  return dp[n][maxWeight];
}

这个函数使用动态规划的方法来解决背包问题,通过构建一个二维数组 dp 来存储不同物品数量和背包容量下的最大价值。最终返回当有 n 个物品且背包容量为 maxWeight 时的最大价值。

  1. const n = scores.length;
  • 确定物品的数量n,通过获取得分数组scores的长度来得到。
  1. const dp = Array.from({ length: n + 1 }, () => Array(maxWeight + 1).fill(0));
  • 创建一个二维数组dp,这个数组的行表示物品的数量(从 0 到n,所以长度是n + 1),列表示背包的不同容量(从 0 到maxWeight,所以长度是maxWeight + 1)。
  • 初始化这个二维数组的所有元素为 0,表示在没有物品或者背包容量为 0 的情况下,最大价值为 0。
  1. 外层循环for (let i = 1; i <= n; i++) {... }
  • 遍历每个物品,从第一个物品开始(因为索引从 1 开始,所以i = 1),直到最后一个物品(i <= n)。
  1. 内层循环for (let w = 0; w <= maxWeight; w++) {... }
  • 遍历不同的背包容量,从 0 开始,直到最大允许的背包容量maxWeight。
  1. if (weights[i - 1] <= w) {... } else {... }
  • 判断当前物品的重量是否小于等于当前考虑的背包容量。
  • 如果当前物品的重量小于等于当前背包容量(即weights[i - 1] <= w),有两种选择:
    • 不选择当前物品,此时最大价值等于不考虑当前物品时的最大价值,即dp[i - 1][w]。
    • 选择当前物品,此时最大价值等于当前物品的得分加上剩余背包容量(背包容量减去当前物品重量)下不考虑当前物品时的最大价值,即scores[i - 1] + dp[i - 1][w - weights[i - 1]]。取这两种选择中的最大值作为当前情况下的最大价值。
  • 如果当前物品的重量大于当前背包容量,那么不能选择当前物品,最大价值就等于不考虑当前物品时的最大价值,即dp[i - 1][w]。
  1. return dp[n][maxWeight];
  • 最终,返回dp[n][maxWeight],它表示在考虑了所有物品并且背包容量为maxWeight的情况下,背包能够携带的最大物品价值。

(二叉树的后续遍历)给定一个二叉树,输出其后续遍历的结果。对于一个二叉树的后续遍历,如果为空树,则返回空字符串,如果不为空,则先遍历左子树,再遍历右子树,最后遍历根节点。

class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = this.right = null;
  }
}

function postorderTraversal(root) {
  const result = [];
  const traverse = (node) => {
    if (node === null) {
      return;
    }
    traverse(node.left);
    traverse(node.right);
    result.push(node.val);
  };
  traverse(root);
  return result;
}

二叉树后序遍历算法的时间复杂度为O(n),其中 n 是二叉树中的节点个数。

  • 类定义部分:
    • 首先定义了一个 TreeNode 类来表示二叉树的节点。每个节点有一个值 val 以及指向左子树和右子树的指针 left 和 right。在构造函数中,可以传入一个值来初始化节点。
  • 函数定义部分:postorderTraversal 函数是用于实现二叉树后序遍历的主要函数。
    • 它首先创建一个空数组 result,用于存储后序遍历的结果。
    • 然后定义了一个内部辅助函数 traverse,这个函数接受一个二叉树节点作为参数。
      • 如果传入的节点为 null,则直接返回,这是递归的边界条件。
      • 如果节点不为 null,则先递归地遍历左子树,即调用traverse(node.left)。这样可以确保先处理左子树的所有节点。
      • 接着递归地遍历右子树,即调用traverse(node.right)。在左子树处理完后处理右子树的节点。
      • 最后,将当前节点的值node.val推入结果数组 result 中。这样就保证了在左子树和右子树都处理完后才处理当前节点。
    • 最后,调用traverse(root)来启动对整个二叉树的后序遍历,并返回结果数组 result。

写一个任务管理器,可以并发执行任务,初始化时传入最大执行并发数。maxcount,限制当前任务并发执行数量。还有个 add 方法,传入一个函数,该函数返回一个 promise,每个函数为一个任务,若当前任务未达到最大并发数,则直接执行任务。若已达到最大并发数,则等待其他任务执行完之后才开始执行。

class TaskManager {
  constructor(maxCount) {
    // 存储最大并发任务数
    this.maxCount = maxCount;
    // 记录当前正在运行的任务数量
    this.runningTasks = 0;
    // 存储等待执行的任务队列
    this.queue = [];
  }
  add(task) {
    if (this.runningTasks < this.maxCount) {
      // 如果当前运行任务数小于最大并发数,直接执行任务
      this.executeTask(task);
    } else {
      // 否则将任务加入等待队列
      this.queue.push(task);
    }
  }
  executeTask(task) {
    this.runningTasks++;
    // 执行任务并返回一个 Promise
    task().then(() => {
      this.runningTasks--;
      // 任务完成后,减少正在运行的任务数量
      if (this.queue.length > 0) {
        // 如果还有等待任务,取出一个并执行
        const nextTask = this.queue.shift();
        this.executeTask(nextTask);
      }
    });
  }
}
  • 在构造函数中,接受一个参数 maxCount,用于初始化任务管理器的最大并发任务数。同时初始化了 runningTasks 为 0,表示当前没有任务在运行,以及一个空数组 queue 用于存储等待执行的任务。
  • add 方法用于添加任务。如果当前正在运行的任务数量小于最大并发数,就立即执行这个任务,调用 executeTask 方法。如果达到了最大并发数,就把任务放入等待队列中。
  • executeTask 方法负责执行单个任务。首先增加正在运行的任务数量,然后执行传入的任务函数(这个任务函数应该返回一个 Promise)。当任务完成后,减少正在运行的任务数量。如果还有等待任务,就从队列中取出一个任务并执行。

add 方法的第二个参数,再加上这个函数执行的优先级,怎么保证它在等待执行的队列当中按照这个顺序去执行?

class TaskManager {
  constructor(maxCount) {
    this.maxCount = maxCount;
    this.runningTasks = 0;
    this.queue = [];
  }
  add(task, priority = 0) {
    if (this.runningTasks < this.maxCount) {
      this.executeTask(task);
    } else {
      // 将任务添加到队列中时考虑优先级
      let inserted = false;
      for (let i = 0; i < this.queue.length; i++) {
        if (priority > this.queue[i].priority) {
          this.queue.splice(i, 0, { task, priority });
          inserted = true;
          break;
        }
      }
      if (!inserted) {
        this.queue.push({ task, priority });
      }
    }
  }
  executeTask(taskWithPriority) {
    this.runningTasks++;
    taskWithPriority.task().then(() => {
      this.runningTasks--;
      if (this.queue.length > 0) {
        const nextTask = this.queue.shift();
        this.executeTask(nextTask);
      }
    });
  }
}
  • add 方法用于添加任务,并接受一个可选的优先级参数。如果当前运行任务数小于最大并发数,就立即执行这个任务,调用 executeTask 方法。如果达到了最大并发数,就需要将任务加入等待队列。在加入等待队列时,首先遍历队列,检查新任务的优先级是否高于当前遍历到的任务。如果是,就将新任务插入到该位置,并设置 inserted 为 true,然后跳出循环。如果遍历完整个队列都没有找到合适位置,说明新任务的优先级不高于任何已在队列中的任务,此时将新任务添加到队列末尾。
  • executeTask 方法负责执行单个任务。现在接收的参数是一个包含任务和优先级的对象 taskWithPriority。首先增加正在运行的任务数量,然后执行传入的任务函数(这个任务函数应该返回一个 Promise)。当任务完成后,减少正在运行的任务数量。如果还有等待任务,就从队列中取出一个任务并执行。