Skip to content

基础题目

1. 函数节流

javascript
function throttle(func, wait) {
  let lastTime = 0;

  return function(...args) {
    const currentTime = Date.now();
    if (currentTime - lastTime >= wait) {
      func.apply(this, args);
      lastTime = currentTime;
    }
  };
}

2. 函数防抖

javascript
function debounce(fn, delay) {
    let timer = null
    return function(...args) {
        let context = this
        if(timer) clearTimeout(timer)
        timer = setTimeout(function(){
            fn.apply(context,args)
            timer = null
        },delay)
    }
}

3. 手写call

javascript
Function.prototype.myCall = function(context) {
  if (typeof context === undefined || typeof context === null) {
    context = window
  }
  const symbol = Symbol()
  context[symbol] = this
  const args = [...arguments].slice(1)
  const result = context[symbol](...args)
  delete context[symbol]
  return result
}

4. 手写apply

javascript
Function.prototype.myApply = function(context) {
  if (typeof context === undefined || typeof context === null) {
    context = window
  }
  const symbol = Symbol()
  context[symbol] = this
  let result
  // 处理参数和 call 有区别
  if (arguments[1]) {
    result = context[symbol](...arguments[1])
  } else {
    result = context[symbol]()
  }
  delete context[symbol]
  return result
}

5. 手写bind

javascript
Function.prototype.myBind = function (context) {
  if (typeof context === undefined || typeof context === null) {
    context = window
  }
  const _this = this
  const args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    // 这边的 apply 严谨点可以自己实现
    return _this.apply(context, args.concat(...arguments))
  }
}

6. 实现instanceof

原型链的向上找,找到原型的最顶端,也就是Object.prototype

javascript
function my_instance_of(leftVaule, rightVaule) {
    if(typeof leftVaule !== 'object' || leftVaule === null) return false;
    let rightProto = rightVaule.prototype,
        leftProto = leftVaule.__proto__;
    while (true) {
        if (leftProto === null) {
            return false;
        }
        if (leftProto === rightProto) {
            return true;
        }
        leftProto = leftProto.__proto__
    }
}

7. 实现new操作

要点

  • 创建一个新对象,这个对象的__proto__要指向构造函数的原型对象
  • 执行构造函数
  • 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象
javascript
function _new() {
    let obj = {};
    let [constructor, ...args] = [...arguments];
    obj.__proto__ = constructor.prototype;
    let result = constructor.apply(obj, args);
    if (result && typeof result === 'function' || typeof result === 'object') {
        return result;
    }
    return obj;
}

8. 实现sleep

某个时间后就去执行某个函数,使用Promise封装

javascript
function sleep(fn, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(fn);
        }, time);
    });
}
let saySomething = (name) => console.log(`hello,${name}`)
async function autoPlay() {
    let demo = await sleep(saySomething('xxx'),1000)
    let demo2 = await sleep(saySomething('xxxx'),1000)
    let demo3 = await sleep(saySomething('xxxxx'),1000)
}
autoPlay()

9. 深拷贝

  • 判断类型,正则和日期直接返回新对象
  • 空或者非对象类型,直接返回原值
  • 考虑循环引用,判断如果hash中含有直接返回hash中的值
  • 新建一个相应的new obj.constructor加入hash
  • 遍历对象递归(普通key和key是symbol情况)
javascript
function deepClone(obj,hash = new WeakMap()){
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    if(obj === null || typeof obj !== 'object') return obj;
    //循环引用的情况
    if(hash.has(obj)){
        return hash.get(obj)
    }
    //new 一个相应的对象
    //obj为Array,相当于new Array()
    //obj为Object,相当于new Object()
    let constr = new obj.constructor();
    hash.set(obj,constr);
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            constr[key] = deepClone(obj[key],hash)
        }
    }
    //考虑symbol的情况
    let symbolObj = Object.getOwnPropertySymbols(obj)
    for(let i=0;i<symbolObj.length;i++){
        if(obj.hasOwnProperty(symbolObj[i])){
            constr[symbolObj[i]] = deepClone(obj[symbolObj[i]],hash)
        }
    }
    return constr
}

10. 函数柯里化

javascript
function curry(fn,...args){
  let fnLen = fn.length,
      argsLen = args.length;
  //对比函数的参数和当前传入参数
  //若参数不够就继续递归返回curry
  //若参数够就调用函数返回相应的值
  if(fnLen > argsLen){
    return function(...arg2s){
      return curry(fn,...args,...arg2s)
    }
  }else{
    return fn(...args)
  }
}

function sumFn(a,b,c){return a+ b + c};
let sum = curry(sumFn);
sum(2)(3)(5)//10
sum(2,3)(5)//10

11. 数组扁平化

javascript
let arr = [1,2,[3,4,[5,[6]]]]
console.log(arr.flat(Infinity))//flat参数为指定要提取嵌套数组的结构深度,默认值为 1
javascript
//用reduce实现
function fn(arr){
   return arr.reduce((prev,cur)=>{
      return prev.concat(Array.isArray(cur)?fn(cur):cur)
   },[])
}

12. 生成随机数

javascript
function getRandom(min, max) {
  return Math.floor(Math.random() * (max - min)) + min   
}

实现数组的随机排序

javascript
let arr = [2,3,454,34,324,32]
arr.sort(randomSort)
function randomSort(a, b) {
  return Math.random() > 0.5 ? -1 : 1;
}

13. 数字转字符串千分位

  1. \d 表示匹配一个数字字符(0-9)。

  2. (?=...) 表示这是一个正向先行断言(positive lookahead),它断言在当前位置后面必须跟着某些特定的字符,但这些字符不会包含在匹配结果中。

  3. (?:\d{3})+ 表示这是一个非捕获组(non-capturing group),它匹配一个或多个连续的三位数字。具体来说:

    • \d{3} 表示匹配三个连续的数字。
    • (?: ... )+ 表示匹配一个或多个非捕获组。
  4. (?:.\d+|$) 表示这是另一个非捕获组,它匹配小数点后跟一个或多个数字,或者字符串的结尾。具体来说:

    • .\d+ 表示匹配一个小数点后跟一个或多个数字。
    • | 表示表示逻辑或。
    • $ 表示匹配字符串的结尾。
javascript
// 输入仅支持整数
function thousandthInteger(num) {
  return num.toString().replace(/\d(?=(?:\d{3})+$)/g, '$&,');
}

// 输入同时支持整数和小数
function thousandth(num) {
  return num.toString().replace(/\d(?=(?:\d{3})+(?:\.\d+|$))/g, '$&,');
}

// 正则表达式 /\d(?=(?:\d{3})+(?:\.\d+|$))/g 中,非捕获组 (?:\d{3})+ 和 (?:\.\d+|$) 用于对匹配进行分组,但不捕获这些组的内容,从而简化了正则表达式的结构,并提高了匹配性能。

关于(?:...) 非捕获组 在正则表达式中,(?:...) 表示一个非捕获组(non-capturing group)。非捕获组的主要作用是对正则表达式的匹配进行分组,但不捕获匹配的内容。这与普通的捕获组 (...) 不同,普通的捕获组会捕获匹配的内容,并将其存储在一个编号的捕获组中,可以在后续的正则表达式操作中引用。

使用非捕获组的好处包括:

  • 性能优化:非捕获组不会存储匹配的内容,因此在某些情况下可以提高正则表达式的匹配性能。
  • 简化引用:在某些复杂的正则表达式中,使用非捕获组可以减少捕获组的数量,从而简化对捕获组的引用。
javascript
// 使用捕获组
let regex1 = /(\d{3})-(\d{2})-(\d{4})/;
let match1 = regex1.exec("123-45-6789");
console.log(match1); // 输出 ["123-45-6789", "123", "45", "6789"]

// 在第一个示例中,正则表达式使用了捕获组 (...),因此匹配的结果包含了三个捕获组的内容:"123"、"45" 和 "6789"。

// 使用非捕获组
let regex2 = /(?:\d{3})-(?:\d{2})-(?:\d{4})/;
let match2 = regex2.exec("123-45-6789");
console.log(match2); // 输出 ["123-45-6789"]



// 在第二个示例中,正则表达式使用了非捕获组 (?:...),因此匹配的结果只包含了整个匹配的内容:"123-45-6789",而没有捕获组的内容。

其他方法

javascript
function numberWithCommas(x) {
    // 转为字符串,按照.拆分
    let arr = (x + '').split(".");
    // 整数部分再拆分
    let int = arr[0].split('');
    // 保存小数部分
    const fraction = arr[1] || '';
    // 记录返回的结果
    let r = '';
    let len = int.length;
    
    // 倒序遍历
    int.reverse().forEach((v, i) => {
        // 非第一位且位值是3的倍数,添加','
        if(i !== 0 && i % 3 === 0) {
            r = v + ',' + r;
        } else {
            r = v + r;
        }
    })
    
    // 返回整数部分 + 小数部分
    return r + (!!fraction ? '.' + fraction : '');
}

// 使用示例
var number = 1234567.89;
var formattedNumber = numberWithCommas(number);
console.log(formattedNumber); // 输出: 1,234,567.89
javascript
console.log((1234567.8911111).toLocaleString('en-US'));  
// 1,234,567.891

console.log((1234567.8911111).toLocaleString('en-US', {
    maximumFractionDigits:10
})); 
// 1,234,567.8911111

14. 用正则实现 trim()

javascript
function trim(string){
    return string.replace(/^\s+|\s+$/g, '')
}

15. 手写一下AJAX

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

16. 手写寄生组合继承

javascript
function inheritPrototype(Child, Parent) {
    // 创建对象,创建父类原型的一个副本
    var prototype = Object.create(Parent.prototype); 
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    prototype.constructor = Child;
    // 指定对象,将新创建的对象赋值给子类的原型
    Child.prototype = prototype; 
}

测试用例

javascript
// 父类初始化实例属性和原型属性
function Father(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
Father.prototype.sayName = function () {
    alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function Son(name, age) {
    Father.call(this, name);
    this.age = age;
}

// 将父类原型指向子类
inheritPrototype(Son, Father);

// 新增子类原型属性
Son.prototype.sayAge = function () {
    alert(this.age);
}

var demo1 = new Son("son1", 21);
var demo2 = new Son("son2", 20);

demo1.colors.push("2"); // ["red", "blue", "green", "2"]
demo2.colors.push("3"); // ["red", "blue", "green", "3"]

17. 10进制转换成[2~16]进制区间数

内置方法:Number.prototype.toString(radix) 可以直接将十进制数转换为指定进制的字符串。

javascript
const decimal = 255;

// 转换为二进制
console.log(decimal.toString(2)); // 输出: "11111111"

// 转换为八进制
console.log(decimal.toString(8)); // 输出: "377"

// 转换为十六进制
console.log(decimal.toString(16)); // 输出: "ff"

// 通用函数
function decimalToBase(decimal, base) {
  if (base < 2 || base > 16) {
    throw new Error('基数必须在2~16之间');
  }
  return decimal.toString(base);
}

console.log(decimalToBase(255, 10)); // 输出: "255"
console.log(decimalToBase(255, 16)); // 输出: "ff"

手动实现转换算法

javascript
function decimalToBase(decimal, base) {
  if (base < 2 || base > 16) {
    throw new Error('基数必须在2~16之间');
  }

  // 处理零的特殊情况
  if (decimal === 0) return '0';

  // 处理负数
  const isNegative = decimal < 0;
  decimal = Math.abs(decimal);

  // 定义进制字符映射
  const digits = '0123456789ABCDEF';
  let result = '';

  // 核心算法:不断取余和整除
  while (decimal > 0) {
    const remainder = decimal % base;
    result = digits[remainder] + result;
    decimal = Math.floor(decimal / base);
  }

  // 添加符号
  return isNegative ? '-' + result : result;
}

// 示例
console.log(decimalToBase(255, 2));  // 输出: "11111111"
console.log(decimalToBase(255, 8));  // 输出: "377"
console.log(decimalToBase(255, 16)); // 输出: "FF"
console.log(decimalToBase(-255, 16)); // 输出: "-FF"

18. 判断对象类型

javascript
let isType = (type) => (obj) => Object.prototype.toString.call(obj) === `[object ${type}]`

// let isArray = isType('Array')
// let isFunction = isType('Function')
// console.log(isArray([1,2,3]),isFunction(Map))
javascript
function getType(obj) {
  // 使用 Object.prototype.toString.call 获取对象类型
  const fullType = Object.prototype.toString.call(obj);
  // 提取类型名称,slice(8, -1):从索引 8 开始(即跳过 [object 部分),到倒数第一个字符(即 ] 之前)结束
  const type = fullType.slice(8, -1);
  return type;
}

// 示例
console.log(getType({})); // Object
console.log(getType([])); // Array
console.log(getType(new Date())); // Date
console.log(getType(null)); // Null
console.log(getType(undefined)); // Undefined
console.log(getType(123)); // Number
console.log(getType('abc')); // String
console.log(getType(true)); // Boolean
console.log(getType(function() {})); // Function

19. 实现数组去重

javascript
let unique = arr => [...new Set(arr)];

20. 实现reduce方法

javascript
Array.prototype.myReduce = function(fn, initVal) {
    let result = initVal,
        i = 0;
    if(typeof initVal  === 'undefined'){
        result = this[i]
        i++;
    }
    while( i < this.length ){
        result = fn(result, this[i])
    }
    return result
}

21. 实现一个同时允许任务数量最大为n的函数

javascript
// tasks数组的每一项是一个Promise对象
function limitRunTask(tasks, n) {
  return new Promise((resolve, reject) => {
    let index = 0, finish = 0, start = 0, res = [];
    function run() {
      if (finish == tasks.length) {
        resolve(res);
        return;
      }
      while (start < n && index < tasks.length) {
        // 每一阶段的任务数量++
        start++;
        let cur = index;
        tasks[index++]().then(v => {
          start--;
          finish++;
          res[cur] = v;
          run();
        });
      }
    }
    run();
  })
}

22. 前端竞态问题

两个信号试着彼此竞争,来影响谁先输出

例如有一个分页列表,快速地切换第二页,第三页; 先后请求 data2 与 data3,分页器显示当前在第三页,并且进入 loading; 但由于网络的不确定性,先发出的请求不一定先响应,所以有可能 data3 比 data2 先返回; 在 data2 最终返回后,分页器指示当前在第三页,但展示的是第二页的数据。

在前端开发中,常见于搜索,分页,选项卡等切换的场景。

当发出新的请求时,取消掉上次请求即可

javascript
function cancelablePromise(promiseArg) {
  let resolve = null
  let reject = null

  const wrappedPromise = new Promise((_resolve, _reject) => {
    resolve = _resolve
    reject = _reject
  })

  promiseArg && promiseArg.then(
    val => {
      resolve && resolve(val)
    },
    error => {
      reject && reject(error)
    }
  )

  return {
    promise: wrappedPromise,
    resolve: (value) => {
      resolve && resolve(value)
    },
    reject: (reason) => {
      reject && reject(reason)
    },
    cancel: () => {
      resolve = null
      reject = null
    }
  }
}

function onlyResolvesLast(fn) {
  // 保存上一个请求的 cancel 方法
  let cancelPrevious = null; 

  const wrappedFn = (...args) => {
    // 当前请求执行前,先 cancel 上一个请求
    cancelPrevious && cancelPrevious();
    // 执行当前请求
    const result = fn.apply(this, args); 
    
    // 创建指令式的 promise,暴露 cancel 方法并保存
    const { promise, cancel } = cancelablePromise(result);
    cancelPrevious = cancel;
    
    return promise;
  };

  return wrappedFn;
}

const fn = (duration) => 
  new Promise(r => {    
    setTimeout(r, duration);  
  });

const wrappedFn = onlyResolvesLast(fn);

wrappedFn(500).then(() => console.log(1));
wrappedFn(1000).then(() => console.log(2));
wrappedFn(100).then(() => console.log(3));

// 输出 3

23. 控制并发数量(p-limit)

javascript
// 简洁版pLimit
// 异步逻辑并行执行,并控制并行数量
// 一个队列来保存任务,有两个时机考虑触发任务执行:
// 1. 开始的时候一次性执行最大并发数的任务 
// 2. 然后每执行完一个启动一个新的
const shortPLimit = (concurrency) => {
  // 传入并发数量
  if (!((Number.isInteger(concurrency) || concurrency === Infinity) && concurrency > 0)) {
    throw new TypeError('Expected `concurrency` to be a number from 1 and up');
  }
  
  // 添加的并发任务要进行排队,所以我们准备一个 queue
  const queue = [];
  // 记录当前在进行中的异步任务
  let activeCount = 0;

  // 下一步处理自然就是把活跃任务数量减一,然后再跑一个任务
  const next = () => {
    activeCount--;

    if (queue.length > 0) {
      // 如果队列中还有任务,就再跑一个
      queue.shift()();
    }
  };

  // 计数,运行这个函数,改变最后返回的那个 promise 的状态
  const run = async (fn, resolve, ...args) => {
    activeCount++;
    // 运行传入的异步函数
    const result = (async () => fn(...args))();

    resolve(result);

    try {
      // 等待异步函数执行完
      await result;
    } catch {}

    // 执行完之后进行下一步处理
    next();
  };

  // 把一个异步任务添加到 queue 中,并且只要没达到并发上限就再执行一批任务
  const enqueue = (fn, resolve, ...args) => {
    queue.push(run.bind(null, fn, resolve, ...args));

    // 为了保证并发数量能控制准确,要等全部的微任务执行完再拿 activeCount 和 queue.length 来判断
    (async () => {
      await Promise.resolve();

      if (activeCount < concurrency && queue.length > 0) {
        // 如果活跃任务数量小于并发数量,就再跑一个
        queue.shift()();
      }
    })();
  };

  // 返回一个添加并发任务的函数,我们把它叫做 generator。
  // 依旧希望返回返回任务函数的promise的结果
  const generator = (fn, ...args) =>
    new Promise((resolve) => {
      enqueue(fn, resolve, ...args);
    });

  
  return generator;
};
javascript
// 完整版pLimit
// 异步逻辑并行执行,并控制并行数量
// 一个队列来保存任务,有两个时机考虑触发任务执行:
// 1. 开始的时候一次性执行最大并发数的任务 
// 2. 然后每执行完一个启动一个新的
const pLimit = (concurrency) => {
    // 传入并发数量
    if (!((Number.isInteger(concurrency) || concurrency === Infinity) && concurrency > 0)) {
      throw new TypeError('Expected `concurrency` to be a number from 1 and up');
    }
    
    // 添加的并发任务要进行排队,所以我们准备一个 queue
    const queue = [];
    // 记录当前在进行中的异步任务
    let activeCount = 0;
  
    // 下一步处理自然就是把活跃任务数量减一,然后再跑一个任务
    const next = () => {
      activeCount--;
  
      if (queue.length > 0) {
        // 如果队列中还有任务,就再跑一个
        queue.shift()();
      }
    };
  
    // 计数,运行这个函数,改变最后返回的那个 promise 的状态
    const run = async (fn, resolve, ...args) => {
      activeCount++;
      // 运行传入的异步函数
      const result = (async () => fn(...args))();

      resolve(result);
  
      try {
        // 等待异步函数执行完
        await result;
      } catch {}

      // 执行完之后进行下一步处理
      next();
    };
  
    // 把一个异步任务添加到 queue 中,并且只要没达到并发上限就再执行一批任务
    const enqueue = (fn, resolve, ...args) => {
      queue.push(run.bind(null, fn, resolve, ...args));
  
      // 为了保证并发数量能控制准确,要等全部的微任务执行完再拿 activeCount 和 queue.length 来判断
      (async () => {
        await Promise.resolve();
  
        if (activeCount < concurrency && queue.length > 0) {
          // 如果活跃任务数量小于并发数量,就再跑一个
          queue.shift()();
        }
      })();
    };
  
    // 返回一个添加并发任务的函数,我们把它叫做 generator
    const generator = (fn, ...args) =>
      new Promise((resolve) => {
        enqueue(fn, resolve, ...args);
      });

    // 为 generator 添加一些属性,方便外部获取当前的状态
    Object.defineProperties(generator, {
      activeCount: {
        get: () => activeCount
      },
      pendingCount: {
        get: () => queue.length
      },
      // 提供了一个清空任务队列的函数
      clearQueue: {
        value: () => {
          queue.length = 0;
        }
      }
    });
    
    return generator;
  };


// 测试代码
const limit = pLimit(2);
  
function asyncFun(value, delay) {
    return new Promise((resolve) => {
        console.log('start ' + value);
        setTimeout(() => resolve(value), delay);
    });
}

(async function () {
    const arr = [
        limit(() => asyncFun('aaa', 2000)),
        limit(() => asyncFun('bbb', 3000)),
        limit(() => asyncFun('ccc', 1000)),
        limit(() => asyncFun('ccc', 1000)),
        limit(() => asyncFun('ccc', 1000))
    ];
  
    const result = await Promise.all(arr);
    console.log(result);
})();

24. 手写Promise

Promise/A+规范:

三种状态 pending| fulfilled(resolved) | rejected 当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态 当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变。

必须有一个支持链式调用的异步 then 方法,then接受两个参数(onFulfilled 用来接收promise成功的值, onRejected 用来接收promise失败的原因)且必须返回一个promise

javascript
// Promise 状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
    constructor(executor) {
        // 初始状态为 pending
        this.status = PENDING;
        // 存储成功的值或失败的原因
        this.value = undefined;
        this.reason = undefined;
        // 存储成功和失败的回调队列
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        // 成功回调
        const resolve = (value) => {
            // 状态只能从 pending 变为 fulfilled
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                // 执行所有成功回调
                this.onFulfilledCallbacks.forEach(callback => callback());
            }
        };

        // 失败回调
        const reject = (reason) => {
            // 状态只能从 pending 变为 rejected
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                // 执行所有失败回调
                this.onRejectedCallbacks.forEach(callback => callback());
            }
        };

        try {
            // 执行 executor 函数
            executor(resolve, reject);
        } catch (error) {
            // 捕获 executor 中的错误并 reject
            reject(error);
        }
    }

    then(onFulfilled, onRejected) {
      // 处理可选参数
      onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;

      onRejected = typeof onRejected === 'function' ? onRejected : error => { 
        throw error; 
      };

      // 创建新的 Promise 用于链式调用
      const newPromise = new MyPromise((resolve, reject) => {
        const handleFulfilled = () => {
            try {
                const x = onFulfilled(this.value);
                // Promise 的链式调用依赖于 then 返回新 Promise,并将上一个 Promise 的结果传递给下一个。
                // resolvePromise 确保无论上一个 then 返回的是普通值还是 Promise,都能正确处理并传递结果。
                // resolvePromise 通过递归等待嵌套 Promise 完成,确保最终结果正确传递。
                resolvePromise(newPromise, x, resolve, reject);
            } catch (error) {
                reject(error);
            }
        };

        const handleRejected = () => {
            try {
                const x = onRejected(this.reason);
                resolvePromise(newPromise, x, resolve, reject);
            } catch (error) {
                reject(error);
            }
        };

        // 根据当前状态处理回调
        if (this.status === FULFILLED) {
            // 使用 setTimeout 确保异步执行
            setTimeout(handleFulfilled, 0);
        } else if (this.status === REJECTED) {
            setTimeout(handleRejected, 0);
        } else {
            // 状态为 pending 时,将回调存入队列
            this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0));
            this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0));
        }
      });

      return newPromise;
    }

    catch(onRejected) {
        return this.then(null, onRejected);
    }

    static resolve(value) {
        return new MyPromise((resolve) => resolve(value));
    }

    static reject(reason) {
        return new MyPromise((_, reject) => reject(reason));
    }

    static all(promises) {
        return new MyPromise((resolve, reject) => {
            const results = [];
            let completedCount = 0;

            if (promises.length === 0) {
                resolve(results);
                return;
            }

            promises.forEach((promise, index) => {
                MyPromise.resolve(promise).then(
                    (value) => {
                        results[index] = value;
                        completedCount++;
                        if (completedCount === promises.length) {
                            resolve(results);
                        }
                    },
                    (reason) => {
                        reject(reason);
                    }
                );
            });
        });
    }

    static race(promises) {
        return new MyPromise((resolve, reject) => {
            promises.forEach((promise) => {
                MyPromise.resolve(promise).then(
                    (value) => {
                        resolve(value);
                    },
                    (reason) => {
                        reject(reason);
                    }
                );
            });
        });
    }
}

// 解析 Promise 结果的辅助函数
function resolvePromise(promise, x, resolve, reject) {
    // 处理循环引用,如果 Promise 直接返回自身,会导致循环调用,例如:
    // const p = new Promise((resolve) => resolve(p));  
    // p.then((val) => console.log(val)); // 无限循环
    if (promise === x) {
        return reject(new TypeError('Chaining cycle detected for promise'));
    }

    
    // Promise 规范要求 resolve/reject 只能被调用一次,多次调用应被忽略。
    let called = false;
    // 处理 thenable 对象,JavaScript 允许自定义 Promise 类(只要实现 then 方法),因此需要兼容这些类 Promise。
    try {
        if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
            const then = x.then;
            if (typeof then === 'function') {
                // 使用 then.call(x) 确保 then 方法中的 this 指向正确。
                then.call(
                    x,
                    (y) => {
                        if (called) return;
                        called = true;
                        // 递归调用 resolvePromise 处理嵌套 Promise(例如 Promise.resolve(Promise.resolve(42)))。
                        resolvePromise(promise, y, resolve, reject);
                    },
                    (r) => {
                        if (called) return;
                        called = true;
                        reject(r);
                    }
                );
            } else {
                resolve(x);
            }
        } else {
            resolve(x);
        }
    } catch (error) {
        if (called) return;
        called = true;
        reject(error);
    }
}

// 测试示例
const promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(42);
    }, 1000);
});

promise
    .then((value) => {
        console.log('Resolved:', value); // 输出: Resolved: 42
        return value * 2;
    })
    .then((value) => {
        console.log('Chained:', value); // 输出: Chained: 84
    })
    .catch((error) => {
        console.error('Rejected:', error);
    });

实现 Promise.resolve

Promise.resolve(value) 可以将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值本身是 Promise 则会原样返回它。

javascript
Promise.resolve(value) {
  if (value && value instanceof Promise) {
    return value;
  } else if (value && typeof value === 'object' && typeof value.then === 'function') {
    let then = value.then;
    return new Promise(resolve => {
      then(resolve);
    });
  } else if (value) {
    return new Promise(resolve => resolve(value));
  } else {
    return new Promise(resolve => resolve());
  }
}

实现Promise.reject

和 Promise.resolve() 类似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。

javascript
Promise.reject = function(reason) {
    return new Promise((resolve, reject) => reject(reason))
}

实现 Promise.all

  • 传入的所有 Promise 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;
  • 只要有一个 Promise 是 rejected,则返回 rejected 状态的新 Promise,且它的值是第一个 rejected 的 Promise 的值;
  • 只要有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise;
javascript
Promise.all = function(promiseArr) {
    let index = 0, result = []
    return new Promise((resolve, reject) => {
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                index++
                result[i] = val
                if (index === promiseArr.length) {
                    resolve(result)
                }
            }, err => {
                reject(err)
            })
        })
    })
}

实现 Promise.race

Promise.race 会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例。

javascript
Promise.race = function(promiseArr) {
    return new Promise((resolve, reject) => {
        promiseArr.forEach(p => {
            Promise.resolve(p).then(val => {
                resolve(val)
            }, err => {
                reject(err)
            })
        })
    })
}

实现Promise.allSettled

特点:

  • 并行执行:所有 Promise 同时开始执行
  • 结果收集:使用数组按原始顺序存储每个 Promise 的结果
  • 错误处理:捕获所有错误,不会因单个 Promise 失败而终止
  • 兼容性:支持传入非 Promise 值,会自动通过 Promise.resolve 包装

核心逻辑:

  • 使用 Promise.resolve 确保每个值都是 Promise
  • 通过 .then 和 .catch 分别处理成功和失败情况
  • 用计数器跟踪完成的 Promise 数量
  • 所有 Promise 完成后一次性返回结果数组
javascript
/**
 * 自定义实现 Promise.allSettled
 * @param {Array<Promise>} promises - Promise 数组
 * @returns {Promise<Array<Object>>} - 结果数组,包含每个 Promise 的状态和值
 */
function promiseAllSettled(promises) {
  // 转换为数组以支持类数组对象
  const promiseArray = Array.from(promises);
  
  // 处理空数组的情况
  if (promiseArray.length === 0) {
    return Promise.resolve([]);
  }
  
  // 存储每个 Promise 的结果
  const results = new Array(promiseArray.length);
  let completedCount = 0;
  
  // 返回一个新的 Promise
  return new Promise((resolve) => {
    // 遍历每个 Promise
    promiseArray.forEach((promise, index) => {
      // 将每个值包装为 Promise,处理非 Promise 值
      Promise.resolve(promise)
        .then((value) => {
          // 成功结果格式
          results[index] = {
            status: 'fulfilled',
            value
          };
        })
        .catch((reason) => {
          // 失败结果格式
          results[index] = {
            status: 'rejected',
            reason
          };
        })
        .finally(() => {
          // 无论成功或失败,计数器加1
          completedCount++;
          
          // 所有 Promise 都已完成,返回结果数组
          if (completedCount === promiseArray.length) {
            resolve(results);
          }
        });
    });
  });
}

// 使用示例
const promises = [
  Promise.resolve(1),
  Promise.reject(new Error('Error occurred')),
  3 // 非 Promise 值会被自动包装
];

promiseAllSettled(promises).then((results) => {
  console.log(results);
  /* 输出:
  [
    { status: 'fulfilled', value: 1 },
    { status: 'rejected', reason: Error: Error occurred },
    { status: 'fulfilled', value: 3 }
  ]
  */
});

25. 排序算法

冒泡排序

冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 2 的位置。

javascript
function bubble(array) {
  checkArray(array);
  for (let i = array.length - 1; i > 0; i--) {
    // 从 0 到 `length - 1` 遍历
    for (let j = 0; j < i; j++) {
      if (array[j] > array[j + 1]) swap(array, j, j + 1)
    }
  }
  return array;
}

快速排序

快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作。

使用额外数组(简洁版)

javascript
function quickSort(arr) {
    if (arr.length <= 1) return arr;
    
    // 选择基准值(取中间元素)
    const pivot = arr[Math.floor(arr.length / 2)];
    const left = [];
    const right = [];
    const equal = [];
    
    // 分区操作
    for (const num of arr) {
        if (num < pivot) {
            left.push(num);
        } else if (num > pivot) {
            right.push(num);
        } else {
            equal.push(num);
        }
    }
    
    // 递归排序并合并结果
    return [...quickSort(left), ...equal, ...quickSort(right)];
}

// 示例用法
const arr = [3, 6, 8, 10, 1, 2, 1];
console.log(quickSort(arr)); // 输出: [1, 1, 2, 3, 6, 8, 10]

原地排序(高效版)

  • 选择基准值:每次选择当前区间的最后一个元素作为基准值
  • 分区操作:
    1. 初始化 storeIndex 为区间起始位置
    2. 遍历区间内所有元素,将小于基准值的元素交换到 storeIndex 位置
    3. 最后将基准值放到 storeIndex 位置,完成分区
  • 递归排序:
    1. 对基准值左边的子数组递归排序
    2. 对基准值右边的子数组递归排序
  • 这种实现保持了原地排序的特性(空间复杂度 O (log n))
javascript
function quickSort(arr) {
    // 辅助函数:递归排序指定区间
    function sort(low, high) {
        if (low >= high) return; // 区间为空或只有一个元素时结束
        
        // 分区操作,获取基准值的最终位置
        const pivotIndex = partition(low, high);
        
        // 递归排序左右两部分
        sort(low, pivotIndex - 1);
        sort(pivotIndex + 1, high);
    }
    
    // 分区函数:将小于基准值的元素放到左边,大于的放到右边
    function partition(low, high) {
        const pivotValue = arr[high]; // 选择最后一个元素作为基准值
        let storeIndex = low; // 存储位置初始化为区间起始位置
        
        // 遍历区间内所有元素(除基准值外)
        for (let i = low; i < high; i++) {
            if (arr[i] < pivotValue) {
                // 将小于基准值的元素交换到存储位置
                [arr[i], arr[storeIndex]] = [arr[storeIndex], arr[i]];
                storeIndex++; // 存储位置后移
            }
        }
        
        // 将基准值放到正确位置
        [arr[storeIndex], arr[high]] = [arr[high], arr[storeIndex]];
        return storeIndex;
    }
    
    // 开始排序整个数组
    sort(0, arr.length - 1);
    return arr;
}

// 示例用法
const arr = [3, 6, 8, 10, 1, 2, 1];
console.log(quickSort(arr)); // 输出: [1, 1, 2, 3, 6, 8, 10]

归并排序

  • 双指针遍历:
    1. 初始化 leftIndex 和 rightIndex 分别指向左右子数组的起始位置。
    2. 每次比较两个指针指向的元素,选择较小的元素放入结果数组。
  • 元素选择规则:
    1. 如果 left[leftIndex] < right[rightIndex],选择左子数组元素。
    2. 否则选择右子数组元素(包括相等的情况),确保稳定排序。
  • 剩余元素处理:
    1. 当其中一个子数组遍历完后,另一个子数组的剩余元素直接追加到结果数组末尾。
    2. 例如:左子数组剩余 [5, 7],右子数组已遍历完,则直接将 [5, 7] 追加到结果中。
javascript
function mergeSort(arr) {
    // 基本情况:数组长度小于等于1时无需排序
    if (arr.length <= 1) return arr;
    
    // 分割数组为左右两部分
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);
    
    // 递归排序左右两部分
    const sortedLeft = mergeSort(left);
    const sortedRight = mergeSort(right);
    
    // 合并已排序的两部分
    return merge(sortedLeft, sortedRight);
}

// 合并两个已排序的数组为一个新的有序数组
function merge(left, right) {
    const result = [];  // 存储合并后的有序数组
    let leftIndex = 0;  // 左子数组的当前索引
    let rightIndex = 0; // 右子数组的当前索引
    
    // 比较左右子数组的元素,按升序依次放入结果数组
    // 只要左右子数组都还有元素未处理,就继续循环
    while (leftIndex < left.length && rightIndex < right.length) {
        if (left[leftIndex] < right[rightIndex]) {
            // 当前左子数组元素更小,将其加入结果数组
            result.push(left[leftIndex]);
            leftIndex++; // 左子数组指针后移
        } else {
            // 当前右子数组元素更小或相等,将其加入结果数组
            // 相等时优先取右子数组元素(稳定排序的关键)
            result.push(right[rightIndex]);
            rightIndex++; // 右子数组指针后移
        }
    }
    
    // 处理剩余元素:
    // 1. 如果左子数组还有剩余元素,直接追加到结果数组末尾
    // 2. 如果右子数组还有剩余元素,直接追加到结果数组末尾
    // 由于左右子数组本身已排序,剩余元素无需比较可直接追加
    return [...result, ...left.slice(leftIndex), ...right.slice(rightIndex)];
}

// 示例用法
const arr = [38, 27, 43, 3, 9, 82, 10];
console.log(mergeSort(arr)); // 输出: [3, 9, 10, 27, 38, 43, 82]

示例 假设我们要合并两个已排序的子数组:

javascript
left = [3, 6, 9]
right = [2, 5, 8, 10]

合并步骤: 初始状态:

javascript
leftIndex = 0, rightIndex = 0
result = []

比较 3 和 2:

javascript
result = [2]
leftIndex = 0, rightIndex = 1

2 更小,将 2 加入结果,右指针后移。

比较 3 和 5:

javascript
result = [2, 3]
leftIndex = 1, rightIndex = 1

3 更小,将 3 加入结果,左指针后移。

比较 6 和 5:

javascript
result = [2, 3, 5]
leftIndex = 1, rightIndex = 2

5 更小,将 5 加入结果,右指针后移。

继续比较...:

最终合并为 [2, 3, 5, 6, 8, 9, 10]。

特性归并排序快速排序堆排序
时间复杂度O(n log n)(稳定)O(n log n)(平均)O(n log n)
空间复杂度O(n)O(log n)(原地排序)O(1)
稳定性稳定不稳定不稳定
适用场景大数据、链表、稳定性要求通用场景、内存敏感大数据排序,内存有限时

堆排序

堆排序的优势在于 原地排序 和 时间复杂度稳定,但实际性能略逊于快速排序(因交换操作更多)。

堆排序算法原理 堆排序基于 二叉堆(Binary Heap) 数据结构,核心是「构建堆」和「提取堆顶元素」两个步骤,整体遵循「选择排序」的思路(每次选最大 / 小值放到对应位置)。

  • 二叉堆的特性

大顶堆:每个父节点的值 ≥ 子节点的值(用于升序排序)。

小顶堆:每个父节点的值 ≤ 子节点的值(用于降序排序)。

堆是完全二叉树,可用数组表示:

索引为 i 的节点,左子节点索引为 2i + 1,右子节点索引为 2i + 2。

最后一个非叶子节点的索引为 Math.floor(n/2) - 1(n 为数组长度)。

  • 算法步骤

    1. Step 1:构建大顶堆

从最后一个非叶子节点开始,依次向上调整每个节点,确保整个数组满足大顶堆特性。

例:[3,1,4,1,5,9,2,6] → 构建后为 [9,5,4,6,1,3,2,1](堆顶为最大值 9)。

  1. Step 2:提取堆顶元素并调整堆

交换堆顶(最大值)与数组末尾元素,此时末尾元素已排序。

缩小堆的范围(排除已排序的末尾元素),重新调整剩余元素为大顶堆。

重复上述操作,直到所有元素排序完成。

javascript
// 堆排序主函数
function heapSort(arr) {
    const len = arr.length;
    
    // 1. 构建大顶堆(从最后一个非叶子节点向上调整)
    for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
        heapify(arr, len, i);
    }
    
    // 2. 逐步提取堆顶元素并调整堆
    for (let i = len - 1; i > 0; i--) {
        // 交换堆顶(最大值)和当前未排序部分的末尾
        [arr[0], arr[i]] = [arr[i], arr[0]];
        // 调整剩余元素为大顶堆(此时未排序部分长度为 i)
        heapify(arr, i, 0);
    }
    
    return arr;
}

// 堆调整函数:确保以 index 为根的子树符合大顶堆特性
function heapify(arr, heapSize, index) {
    let largest = index; // 初始化最大值为根节点
    const left = 2 * index + 1; // 左子节点索引
    const right = 2 * index + 2; // 右子节点索引
    
    // 比较左子节点与根节点,更新最大值索引
    if (left < heapSize && arr[left] > arr[largest]) {
        largest = left;
    }
    
    // 比较右子节点与当前最大值,更新最大值索引
    if (right < heapSize && arr[right] > arr[largest]) {
        largest = right;
    }
    
    // 如果最大值不是根节点,则交换并递归调整子树
    if (largest !== index) {
        [arr[index], arr[largest]] = [arr[largest], arr[index]];
        heapify(arr, heapSize, largest);
    }
}

// 示例用法
const arr = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(heapSort(arr)); // 输出: [1, 1, 2, 3, 4, 5, 6, 9]

26. 实现一个定时器(尾递归+requestAnimationFrame)

window.requestAnimationFrame()  会告诉浏览器我们希望执行一个 动画,并且要求浏览器在下次 重绘之前 调用指定的回调函数 更新动画,通常 60Hz,约 16.7ms/帧 执行一次回调函数。

为了提高性能和电池寿命,在大多数浏览器里,当 requestAnimationFrame() 运行在 后台标签页 或 隐藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

浏览器内部以 32 位带符号整数 存储延时,这会导致如果一个延时大于 2147483647 ms(大约 24.8 天) 时会产生溢出,导致定时器将会被 立即执行,这个限制适用于 setInterval() 和 setTimeout()。

此外,还有有很多因素会导致 setTimeout 的 回调函数 执行 比设定的预期值更久: setTimeout 的 嵌套调用达到 5 层、浏览器会在非活动标签中、系统负载过重、页面被隐藏(如 minimize 状态)等等

以下方案优势:

  • 与屏幕刷新率同步,避免丢帧,动画更流畅
  • 页面不可见时自动暂停,节省资源
  • 高精度时间控制,适合需要精确计时的场景
javascript
function mySetInterval(callback, interval) {
    let startTime = performance.now();
    function loop() {
        const currentTime = performance.now();
        if (currentTime - startTime >= interval) {
            callback();
            startTime = currentTime;
        }
        requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
}
javascript
function mySetTimeout(callback, delay) { 
  let startTime = performance.now();
  function loop() {
    let currentTime = performance.now();
    if (currentTime - startTime >= delay) {
      callback();
    } else {
      requestAnimationFrame(loop);
    }
  }
}

合并setInterval和setTimeout实现:

javascript
function myTimer(callback, delay, options = {}) {
    const { repeat = false } = options;
    let startTime = performance.now();
    let taskId;
    
    function loop() {
        const currentTime = performance.now();
        if (currentTime - startTime >= delay) {
            callback();
            if (repeat) {
                startTime = currentTime; // 重置时间,继续循环
                taskId = requestAnimationFrame(loop);
            }
            // 非 repeat 模式下自然结束,不继续请求下一帧
        } else {
            taskId = requestAnimationFrame(loop);
        }
    }
    
    taskId = requestAnimationFrame(loop);
    
    return () => cancelAnimationFrame(taskId);
}

// 使用示例
const cancelInterval = myTimer(() => console.log('每秒执行'), 1000, { repeat: true });
const cancelTimeout = myTimer(() => console.log('3秒后执行一次'), 3000);
javascript
/**
 * 完整自定义定时器实现
 */
class Timer {
  constructor() {
    this.tasks = new Map();
    this.taskId = 0;
  }

  /**
   * 实现类似 setTimeout 的功能
   * @param {Function} callback - 回调函数
   * @param {number} delay - 延迟时间(毫秒)
   * @returns {number} - 任务 ID
   */
  setTimeout(callback, delay) {
    const id = this.taskId++;
    const startTime = performance.now();
    
    const executeTask = () => {
      const currentTime = performance.now();
      if (currentTime - startTime >= delay) {
        callback();
        this.tasks.delete(id);
      } else {
        this.tasks.set(id, requestAnimationFrame(executeTask));
      }
    };
    
    this.tasks.set(id, requestAnimationFrame(executeTask));
    return id;
  }

  /**
   * 实现类似 setInterval 的功能
   * @param {Function} callback - 回调函数
   * @param {number} interval - 间隔时间(毫秒)
   * @returns {number} - 任务 ID
   */
  setInterval(callback, interval) {
    const id = this.taskId++;
    let lastTime = performance.now();
    
    const executeTask = () => {
      const currentTime = performance.now();
      if (currentTime - lastTime >= interval) {
        callback();
        lastTime = currentTime;
      }
      this.tasks.set(id, requestAnimationFrame(executeTask));
    };
    
    this.tasks.set(id, requestAnimationFrame(executeTask));
    return id;
  }

  /**
   * 清除定时器
   * @param {number} id - 任务 ID
   */
  clearTimer(id) {
    if (this.tasks.has(id)) {
      cancelAnimationFrame(this.tasks.get(id));
      this.tasks.delete(id);
    }
  }
}

// 使用示例
const timer = new Timer();

// 测试 setTimeout
const timeoutId = timer.setTimeout(() => {
  console.log('setTimeout 触发');
}, 2000);

// 测试 setInterval
const intervalId = timer.setInterval(() => {
  console.log('setInterval 触发');
}, 1000);

// 取消定时器示例
timer.clearTimer(intervalId);

27. 简要实现 CountDown 计时器组件

针对一个 CountDown 计时器组件, props 参数

  • time 即需要倒计时的时间,时间我们可以直接限制为 时间戳,数值类型
  • format,即输出的时间格式,支持 DD:HH:mm:ss:SSS 格式
  • finish 事件,即倒计时结束时会被执行的事件
  • slot 默认插槽,即需要展示的组件内容视图,可接收到内部的倒计时格式输出

src\components\CountDown\index.vue

javascript
<template>
 <div class="count-down">
   <slot v-bind="currentTime">
     <h1>{{ currentTime.format }}</h1>
   </slot>
 </div>
</template>

<script setup>
import { computed, ref, onMounted } from 'vue'
import useCountDown from './Composable/useCountDown'

const props = defineProps({
 time: {
   type: Number,
   default: 0,
 },
 format: {
   type: String,
   default: 'DD:HH:mm:ss:SSS',
 },
 immediate: {
   type: Boolean,
   default: true,
 },
})

const emits = defineEmits(['finish'])

const { start, currentTime } = useCountDown({
 ...props,
 onFinish: () => emits('finish'),
})

// 判断是否需要立即执行
onMounted(() => {
 if (props.immediate) start()
})

// 向外部暴露的内容
defineExpose({
 start,
 currentTime,
})
</script>

src\components\CountDown\composable\useCountDown\index.ts

javascript
import { computed, ref } from 'vue'
import { parseTime, formatTime } from '../../utils'


export default (options) => {
    // 是否正在倒计时
    let counting = false

    // 剩余时间
    const remain = ref(options.time)

    // 结束时间
    const endTime = ref(0)

    // 格式化输出的日期时间
    const currentTime = computed(() => formatTime(options.format, parseTime(remain.value)))

    // 获取当前剩余时间
    const getCurrentRemain = () => Math.max(endTime.value - Date.now(), 0)

    // 设置剩余时间
    const setRemain = (value) => {

        // 更新剩余时间
        remain.value = value

        // 倒计时结束
        if (value === 0) {
            // 触发 Finish 事件
            options.onFinish?.()

            // 正在倒计时标志为 false
            counting = false
        }
    }

    // 倒计时
    const tickTime = () => {
        requestAnimationFrame(() => {
            // 更新剩余时间
            setRemain(getCurrentRemain())

            // 倒计时没结束,就继续
            if (remain.value > 0) {
                tickTime()
            }
        })
    }

    // 启动
    const start = () => {
        // 正在倒计时,忽略多次调用 start 
        if (counting) return

        // 正在倒计时标志为 true
        counting = true

        // 设置结束时间
        endTime.value = Date.now() + remain.value

        // 开启倒计时
        tickTime()
    }

    return {
        currentTime,
        start
    }

}

src\components\CountDown\utils\index.ts

javascript
// 常量
const SECOND = 1000
const MINUTE = 60 * SECOND
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR

// 解析时间
export const parseTime = (time) => {
  const days = Math.floor(time / DAY)
  const hours = Math.floor((time % DAY) / HOUR)
  const minutes = Math.floor((time % HOUR) / MINUTE)
  const seconds = Math.floor((time % MINUTE) / SECOND)
  const milliseconds = Math.floor(time % SECOND)

  return {
    days,
    hours,
    minutes,
    seconds,
    milliseconds,
  }
}

// 格式化时间
export const formatTime = (format, time) => {
  let { days, hours, minutes, seconds, milliseconds } = time

  // 判断是否需要展示 天数,需要则补 0,否则将 天数 降级加到 小时 部分
  if (format.includes('DD')) {
    format = format.replace('DD', padZero(days))
  } else {
    hours += days * 24
  }

  // 判断是否需要展示 小时,需要则补 0,否则将 小时 降级加到 分钟 部分
  if (format.includes('HH')) {
    format = format.replace('HH', padZero(hours))
  } else {
    minutes += hours * 60
  }

  // 判断是否需要展示 分钟,需要则补 0,否则将 分钟 降级加到 秒数 部分
  if (format.includes('mm')) {
    format = format.replace('mm', padZero(minutes))
  } else {
    seconds += minutes * 60
  }

  // 判断是否需要展示 秒数,需要则补 0,否则将 秒数 降级加到 毫秒 部分
  if (format.includes('ss')) {
    format = format.replace('ss', padZero(seconds))
  } else {
    milliseconds += seconds * 1000
  }

  // 默认展示 3位 毫秒数
  if (format.includes('SSS')) {
    const ms = padZero(milliseconds, 3)
    format = format.replace('SSS', ms)
  }

  // 最终返回格式化的数据
  return { format, days, hours, minutes, seconds, milliseconds }
}

// 不足位数用 0 填充
export const padZero = (str, padLength = 2) => {
  str += ''
  if (str.length < padLength) {
    str = '0'.repeat(padLength - str.length) + str
  }
  return str
}

28. 实现数据分批渲染

在处理大量数据渲染时,可利用尾递归和requestAnimationFrame实现分时渲染,减轻浏览器负担。

javascript
// 将 1000 条数据分批,每次渲染 10 条,通过requestAnimationFrame的尾递归调用,在每一帧重绘前渲染一批数据,直至数据渲染完毕。
const data = Array.from({ length: 1000 }, (_, i) => i);
const ul = document.querySelector('#list - with - big - data');

function renderData(chunk) {
    const fragment = document.createDocumentFragment();
    chunk.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item;
        fragment.appendChild(li);
    });
    ul.appendChild(fragment);
}

function batchRender(data, batchSize) {
    if (data.length === 0) {
        return;
    }
    const batch = data.splice(0, batchSize);
    renderData(batch);
    requestAnimationFrame(() => batchRender(data, batchSize));
}

batchRender(data, 10);

29. 实现一个 executeTasks 函数

  • 接收任务数组 tasks(每个任务为返回 Promise 的函数)和重试次数 retries 作为参数。
  • 串行执行所有任务(即前一个任务完成后才执行下一个)。
  • 若某个任务执行成功(Promise resolve),则继续执行下一个任务。
  • 若某个任务执行失败(Promise reject),则最多重试 retries 次:
    • 若重试后成功,继续执行后续任务;
    • 若重试 retries 次后仍失败,终止整个函数并抛出最后一次错误。
javascript
async function executeTasks(tasks, retries) {
    for (let i = 0; i < tasks.length; i++) {
        let currentRetries = 0;
        while (currentRetries <= retries) { // 重试循环
            try {
                await tasks[i](); // 执行任务
                break; // 成功则跳出重试循环
            } catch (error) {
                if (currentRetries === retries) { // 达到最大重试次数
                    throw error; // 抛出最终错误
                }
                currentRetries++; // 否则增加重试计数,继续循环
            }
        }
    }
    return Promise.resolve();
}

// 示例用法
executeTasks([task1, task2, task3], 2); // 每个任务最多重试2次

30. 实现 Compile 函数

将模板字符串中的插值(如 {{user.name}})编译为具体的值。例如,输入模板 "Hello, {{user.name}}!" 和数据 {user: {name: "Alice"}},输出 "Hello, Alice!"

实现思路:

  • 正则匹配插值:使用正则表达式(如 /\{\{([^}]+)\}\}/g)定位模板中的插值表达式,提取变量路径(如 user.name)。

  • 数据路径解析:将变量路径按 . 分割为层级数组(如 ["user", "name"]),逐层访问数据对象的属性。

replace(pattern, replacement)

replacement可以是字符串或函数。

  • 如果是字符串,则将匹配项替换为字符串。替换字符串可以包括特殊替换模式,例如$& 表示匹配的子串。
  • 如果是函数,则将匹配项替换为函数的返回值。函数的参数是匹配项、匹配项在字符串中的索引、匹配项在字符串中的起始位置和原始字符串,一个捕获组命名组成的对象。
javascript
function replacer(match, p1, p2, /* …, */ pN, offset, string, groups) {
  return replacement;
}
javascript
function compile(template) {
  // ([^}]+) 匹配 {{}} 中的内容
  // 这是一个捕获组,它的作用是将括号内匹配到的内容提取出来。在后续使用 replace 方法时,可以通过回调函数获取到这个捕获组中的内容。
  // [^}]:^ 在方括号 [] 内使用时,表示取反的意思。[^}] 表示匹配除了右花括号 } 之外的任意字符。
  // +:表示前面的元素(即 [^}])可以出现一次或多次。所以 [^}]+ 表示匹配一个或多个非右花括号的字符。
  const regex = /\{\{([^}]+)\}\}/g;
  return function(data) {
    return template.replace(regex, (match, path) => {
      const keys = path.trim().split('.');
      // 根据 keys 数组中的路径层级,逐层访问 data 对象的属性,以获取最终的值。若过程中出现属性不存在的情况,会返回空字符串。
      return keys.reduce((obj, key) => obj?.[key], data) || '';
    });
  };
}

// 使用示例
const template = "Hello, {{user.name}}! Age: {{age}}";
const render = compile(template);
console.log(render({ user: { name: "Alice" }, age: 25 })); 
// 输出: "Hello, Alice! Age: 25"