【前端面试】手写题

465 阅读19分钟

运行结果题

考察作用域

题目一

代码:

var foo = 1;
function fn() {
  foo = 3;
  return;
  function foo() {
    // todo
  }
}
fn();
console.log(foo);

分析: 因为 fn 函数内部对 foo 的赋值操作不会影响全局作用域中的 foo 变量。

结果:

// 输出 1

题目二

代码:

var a = 10;
function test() {
  console.log(a);
  var a = 20;
  console.log(a);
}
test();

分析:

  1. 函数作用域中 var a 会变量提升,但不会提升赋值。
  2. 因此第一次打印时 a 已声明未赋值 → undefined
  3. 第二次打印时 a 已被赋值为 20

结果:

// undefined
// 20

考察事件循环

题目一

代码:

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

async1();

new Promise(function (resolve) {
  console.log('promise1');
  resolve();
}).then(function () {
  console.log('promise2');
});

console.log('script end');

结果:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

题目二

代码:

async function test() {
  console.log(1);
  await new Promise((resolve) => {
    console.log(2);
    resolve();
  }).then(() => {
    console.log(3);
  });
  console.log(4);
}
test();
console.log(5);

分析:

  • 12 同步执行;
  • await 后暂停,主线程继续执行 → 输出 5
  • .then() 回调进入微任务队列 → 输出 3
  • await 继续执行 → 输出 4

结果:

1
2
5
3
4

题目三

代码:

setTimeout(function () {
  console.log('1');
}, 0);

async function async1() {
  console.log('2');
  const data = await async2();
  console.log('3');
  return data;
}

async function async2() {
  return new Promise((resolve) => {
    console.log('4');
    resolve('async2 的结果');
  }).then((data) => {
    console.log('5');
    return data;
  });
}

async1().then((data) => {
  console.log('6');
  console.log(data);
});

new Promise(function (resolve) {
  console.log('7');
  // resolve()
}).then(function () {
  console.log('8');
});

分析:

主线程同步执行:

  • setTimeout(...) // 宏任务,延迟执行
  • async1() // 调用 async1
    • console.log("2") → 输出 2
    • await async2()
  • async2() // 调用 async2
    • console.log("4") → 输出 4
    • resolve.then(...) 放入微任务
  • new Promise(...) // 执行同步函数
    • console.log("7") → 输出 7
    • then 不触发(未 resolve)

微任务队列执行:

  • async2().then(...)console.log("5") → 输出 5
  • await async2() 后续 → console.log("3") → 输出 3
  • async1().then(...)console.log("6") → 输出 6
  • console.log(data) → 输出 async2 的结果

宏任务队列执行:

  • setTimeoutconsole.log("1") → 输出 1

结果:

2
4
7
5
3
6
async2 的结果
1

题目四

代码:

async function testAwait() {
  console.log('1. async 函数开始');

  // 遇到 await,先执行 promise 同步代码,再暂停函数
  const result = await new Promise((resolve) => {
    console.log('2. promise 内部同步代码');
    // 模拟异步操作(如接口请求)
    setTimeout(() => {
      console.log('5. promise 异步操作完成');
      resolve('数据');
    }, 1000);
  });

  // 这部分代码会被包装成微任务,等 promise 完成后执行
  console.log('6. await 恢复,拿到结果:', result);
}

// 执行 async 函数
testAwait();

// 3. await 暂停时,主线程执行后面的同步代码
console.log('3. 主线程处理同步代码');

// 4. 主线程处理宏任务(setTimeout)
setTimeout(() => {
  console.log('4. 主线程处理其他宏任务');
}, 500);

结果:

1. async 函数开始
2. promise 内部同步代码
3. 主线程处理同步代码
4. 主线程处理其他宏任务
5. promise 异步操作完成
6. await 恢复,拿到结果: 数据

考察 Promise(高)

题目一

代码:

const p = new Promise((resolve, reject) => {
  console.log(0);
  reject();
  console.log(1);
  resolve();
  console.log(2);
});
p.then((res) => {
  console.log(3);
})
  .then((res) => {
    console.log(4);
  })
  .catch((res) => {
    console.log(5);
  })
  .then((res) => {
    console.log(6);
  })
  .catch((res) => {
    console.log(7);
  })
  .then((res) => {
    console.log(8);
  });

分析:

  1. rejectresolve 不会影响 promise 函数体内的代码执行所以输出 0 1 2
  2. 由于先有 reject 所以前两个 then 链式调用不会有输出,直接到第一个 catch 输出 5
  3. 由于上一段代码没有错误,所以正常 then 输出 68

结果:

0
1
2
5
6
8

题目二

代码:

class Scheduler {
  constructor(limit) {
    this.limit = limit;
    this.queue = [];
    this.running = 0;
  }

  add(task) {
    return new Promise((resolve) => {
      const run = async () => {
        this.running++;
        const res = await task();
        resolve(res);
        this.running--;
        if (this.queue.length) {
          this.queue.shift()();
        }
      };
      if (this.running < this.limit) {
        run();
      } else {
        this.queue.push(run);
      }
    });
  }
}

const timeout = (time) => new Promise((r) => setTimeout(r, time));

const scheduler = new Scheduler(2);
const addTask = (time, name) =>
  scheduler.add(() => timeout(time).then(() => console.log(name)));

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');

分析:

  • 同时最多运行 2 个任务。
  • 起初执行 1, 22 先完成,触发 3
  • 接着 1 完成,触发 4
  • 最终执行顺序:2314

结果:

2
3
1
4

题目三

代码:

Promise.resolve()
  .then(() => {
    console.log(1);
    return Promise.resolve(2);
  })
  .then((res) => {
    console.log(res);
    return 3;
  })
  .then(console.log);

Promise.resolve()
  .then(() => {
    console.log('A');
    return Promise.reject('B');
  })
  .catch((err) => {
    console.log('C', err);
  })
  .then(() => {
    console.log('D');
  });

分析:

第一个链:

  • 输出 1;返回 Promise.resolve(2)
  • 等待完成 → 输出 2
  • 下一次 then → 输出 3

第二个链:

  • 输出 A;返回 reject('B') → 进入 catch
  • 输出 C B
  • catch 返回 resolved 状态 → then 执行 → 输出 D

结果:

1
A
2
C B
3
D

字符串处理

题目一:字符串连续重复字符输出

代码:

function compressString(str) {
  let res = '';
  let count = 1;

  for (let i = 0; i < str.length; i++) {
    if (str[i] === str[i + 1]) {
      count++;
    } else {
      res += str[i] + count + ' ';
      count = 1;
    }
  }

  return res.trim();
}

console.log(compressString('aaabbcdddde'));

分析:

  1. 初始化计数 count = 1;遍历字符串逐个对比相邻字符。
  2. 如果当前字符与下一个相同 → 计数累加。
  3. 否则说明一段连续字符结束,将该字符和次数拼接到结果字符串中,并重置 count = 1
  4. 末尾返回完整拼接的字符串。

结果:

a3 b2 c1 d4 e1

题目二:输出字符、开始结束位置

代码:

function getCharRanges(str) {
  const res = [];
  let start = 0;

  for (let i = 1; i <= str.length; i++) {
    if (str[i] !== str[i - 1]) {
      res.push({
        char: str[start],
        start,
        end: i - 1
      });
      start = i;
    }
  }

  return res;
}

console.log(getCharRanges('aaabbcdddde'));

分析:

  1. start 记录当前连续字符的起点。
  2. 从第二个字符开始遍历,如果当前字符与前一个不同,说明一段结束。
  3. 将当前段的 charstartend 推入结果数组。
  4. start 移动到下一个段的起始位置。

结果:

[
  { "char": "a", "start": 0, "end": 2 },
  { "char": "b", "start": 3, "end": 4 },
  { "char": "c", "start": 5, "end": 5 },
  { "char": "d", "start": 6, "end": 9 },
  { "char": "e", "start": 10, "end": 10 }
]

async / await 执行顺序

题目一

代码:

async function async1() {
  console.log('A');
  await async2();
  console.log('B');
}

async function async2() {
  console.log('C');
}

console.log('D');
async1();
console.log('E');

分析:

  1. console.log('D') → 同步执行。
  2. 调用 async1() 输出 A
  3. 执行 await async2(),进入 async2,输出 Cawait 后面的语句(B)被放入微任务队列。
  4. 同步任务结束后执行 console.log('E')
  5. 最后微任务队列中执行 console.log('B')

结果:

D
A
C
E
B

考察 this 指向

题目一

代码:

var name = 'window';

const obj = {
  name: 'obj',
  getName() {
    return function () {
      console.log(this.name);
    };
  }
};

obj.getName()();

分析:

  1. getName() 返回的是一个普通函数,而不是箭头函数;
  2. 当执行 obj.getName()() 时,第二次调用独立执行,this 指向全局对象;
  3. 因此输出 window
  4. getNamereturn 改成箭头函数则输出 obj

结果:

window

函数组合 / 柯里化执行结果

题目一

代码:

function add(a) {
  return function (b) {
    return function (c) {
      console.log(a, b, c);
      return a + b + c;
    };
  };
}

console.log(add(1)(2)(3));

分析:

  1. 每次调用返回下一级函数;
  2. 最后一级函数执行时,闭包保留前面参数;
  3. 执行 console.log(a,b,c) 输出 1 2 3
  4. 返回相加结果 6

结果:

1 2 3
6

防抖 / 节流执行顺序

题目一

代码:

function debounce(fn, delay) {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

const log = debounce(() => console.log('run'), 200);

log();
log();
log();
setTimeout(log, 300);

分析:

  1. 前三次快速连续触发,前两次被清除,只有最后一次触发定时器执行;
  2. 300ms 再次触发时,前一次已经执行完,因此又执行一次;
  3. 所以共执行两次 "run"

结果:

run
run

sleep / get 执行顺序

题目一

代码:

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run() {
  console.log('A');
  await sleep(100);
  console.log('B');
  await sleep(0);
  console.log('C');
}

run();
console.log('D');

分析:

  • A:同步执行;
  • Dawait 会让出执行权,主线程继续;
  • Bsleep(100) 结束后异步执行;
  • Csleep(0) 结束后微任务执行;

结果:

A
D
B
C

数组方法执行结果

题目一

代码:

const arr = [1, 2, [3, 4, [5]]];

const result = arr.flat(2).reduce((acc, cur) => acc + cur, 0);

console.log(result);

分析:

  • flat(2) → 深度拍平 2 层:[1, 2, 3, 4, 5];
  • reduce 从 0 开始累计相加;
  • 得到结果 15

结果:

15

题目二

代码:

function group(arr, fn) {
  return arr.reduce((acc, cur) => {
    const key = fn(cur);
    if (!acc[key]) acc[key] = [];
    acc[key].push(cur);
    return acc;
  }, {});
}

const data = [6.1, 4.2, 6.3];
console.log(group(data, Math.floor));

分析:

  • reduce 遍历数组;
  • 通过 fn(cur) 计算分组键;
  • 同键的元素放入同一数组;
  • 典型函数式思维实现分组逻辑。

结果:

{
  "4": [4.2],
  "6": [6.1, 6.3]
}

题目三

代码:

const arr = [1, [2, [3, 4]]];
const res = arr.flat().reduce((a, b) => a.concat(b), []);
console.log(res);

分析:

  • flat() 默认深度为 1,只展开一层;
  • 因此数组变为 [1, 2, [3, 4]]
  • reduceconcat 合并元素,但 [3,4] 仍为数组;
  • 没有完全拍平。

结果:

[1, 2, [3, 4]]

手撕代码题

手写 Call / Apply / Bind

手写 Call

call()方法允许调用具有 this 值和参数的函数。

  • thisArg:要将函数内的 this 关键字指向的对象。
  • arg1, arg2, ...:要传递给函数的参数。
Function.prototype.myCall = function (thisArg, ...args) {
  thisArg = thisArg || window;
  thisArg.fn = this;
  const result = thisArg.fn(...args);
  delete thisArg.fn;
  return result;
};

手写 Apply

apply()方法与 call()方法类似,但接受一个参数数组而不是一系列单独的参数。

  • thisArg:要将函数内的 this 关键字指向的对象。
  • [arg1, arg2, ...]:一个数组,包含要传递给函数的参数。
Function.prototype.myApply = function (thisArg, argsArray) {
  thisArg = thisArg || window;
  thisArg.fn = this;
  const result = thisArg.fn(...(argsArray || []));
  delete thisArg.fn;
  return result;
};

手写 Bind

bind()方法创建了一个新的函数,其中 this 关键字被绑定到传递给 bind()方法的参数中的值。

  • thisArg:要将函数内的 this 关键字指向的对象。
  • arg1, arg2, ...:要在调用新函数时传递的参数。
Function.prototype.myBind = function (thisArg, ...args) {
  let self = this;
  return function (...newArgs) {
    // 在 new 场景下,this 指向实例
    const isNew = this instanceof self;
    const context = isNew ? this : Object(thisArg);

    // 为了避免污染,使用唯一 key
    const fnKey = Symbol('fn');
    context[fnKey] = self;

    const result = context[fnKey](...args, ...newArgs);
    delete context[fnKey];

    return result;
  };
};

手写 New

// 模拟 new 关键字的函数
function myNew(constructor, ...args) {
  // 1. 创建一个新对象,并将新对象的原型指向构造函数的原型
  const obj = Object.create(constructor.prototype);

  // 2. 执行构造函数,将 this 绑定到新对象
  const result = constructor.apply(obj, args);

  // 3. 如果构造函数返回了对象,则返回该对象,否则返回新创建的对象
  return typeof result === 'object' && result !== null ? result : obj;
}

手写防抖

多次触发合并为最后一次执行(如搜索输入联想)。

// 防抖函数
function debounce(fn, delay) {
  let timer = null; // 用于保存定时器

  return function (...args) {
    // 如果已有定时器,清除它(重新计时)
    if (timer) clearTimeout(timer);

    // 设置新的定时器,延迟执行原函数
    timer = setTimeout(() => {
      fn.apply(this, args); // 确保this指向正确
      timer = null; // 执行后清空定时器
    }, delay);
  };
}

手写节流

控制执行频率,固定间隔执行一次(如滚动加载、高频点击)。

// 节流函数(定时器版)
function throttleTimer(fn, interval) {
  let timer = null;

  return function (...args) {
    // 如果没有定时器,则设置一个
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行后清空定时器
      }, interval);
    }
  };
}

用 setTimeout 模拟实现 setInterval

function mySetInterval(fn, time = 1000) {
  let timer = null;

  function interval() {
    fn();
    timer = setTimeout(interval, time);
  }

  timer = setTimeout(interval, time);

  // 返回一个函数用于清除定时器
  return () => {
    clearTimeout(timer);
  };
}

// 使用示例:
// let cancel = mySetInterval(() => {
//     console.log(111);
// }, 1000);
//
// setTimeout(() => {
//     cancel(); // 停止 interval
// }, 5000);

将虚拟 Dom 转化为真实 Dom

虚拟 DOM 结构:

{
  "tag": "DIV",
  "attrs": {
    "id": "app"
  },
  "children": [
    {
      "tag": "SPAN",
      "children": [{ "tag": "A", "children": [] }]
    },
    {
      "tag": "SPAN",
      "children": [
        { "tag": "A", "children": [] },
        { "tag": "A", "children": [] }
      ]
    }
  ]
}

目标真实 DOM:

<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

实现:

// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === 'number') {
    vnode = String(vnode);
  }
  // 字符串类型直接就是文本节点
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作 这一步是关键
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

手写 Promise

  • 状态管理pendingfulfilled/rejected,状态不可逆。
  • 微任务异步回调queueMicrotask 保证 then 异步执行。
  • 链式调用:每个 then 返回新 Promise,可继续链式调用。
  • 异常捕获executor 或回调抛错会进入 reject
class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (val) => {
      if (this.status !== 'pending') return;
      this.status = 'fulfilled';
      this.value = val;
      this.onFulfilledCallbacks.forEach((fn) => fn());
    };

    const reject = (err) => {
      if (this.status !== 'pending') return;
      this.status = 'rejected';
      this.reason = err;
      this.onRejectedCallbacks.forEach((fn) => fn());
    };

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

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (e) => {
            throw e;
          };

    return new MyPromise((resolve, reject) => {
      const handle = (callback, value) => {
        queueMicrotask(() => {
          try {
            const result = callback(value);
            // 处理 then 返回 Promise 的情况
            if (result instanceof MyPromise) {
              result.then(resolve, reject);
            } else {
              resolve(result);
            }
          } catch (err) {
            reject(err);
          }
        });
      };

      if (this.status === 'fulfilled') {
        handle(onFulfilled, this.value);
      } else if (this.status === 'rejected') {
        handle(onRejected, this.reason);
      } else {
        this.onFulfilledCallbacks.push(() => handle(onFulfilled, this.value));
        this.onRejectedCallbacks.push(() => handle(onRejected, this.reason));
      }
    });
  }
}

// 测试
new MyPromise((resolve) => setTimeout(() => resolve(42), 500)).then((res) =>
  console.log('成功:', res)
);

手写深拷贝

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj; // 如果是基础类型或null,直接返回
  }

  if (hash.has(obj)) {
    return hash.get(obj); // 解决循环引用
  }

  let clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone); // 存储当前对象的拷贝

  // 考虑 Symbol 类型的 key
  const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];

  for (let key of keys) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      clone[key] = deepClone(obj[key], hash); // 递归拷贝
    }
  }

  return clone;
}

手写发布订阅

class PubSub {
  constructor() {
    // 存储事件和对应的订阅者
    this.events = {};
  }

  /**
   * 订阅事件
   * @param {string} event 事件名称
   * @param {Function} callback 回调函数
   * @returns {Function} 用于取消订阅的函数
   */
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);

    // 返回取消订阅的函数
    return () => {
      this.unsubscribe(event, callback);
    };
  }

  /**
   * 取消订阅
   * @param {string} event 事件名称
   * @param {Function} callback 要取消的回调
   */
  unsubscribe(event, callback) {
    if (!this.events[event]) {
      return;
    }
    this.events[event] = this.events[event].filter((cb) => cb !== callback);
  }

  /**
   * 发布事件
   * @param {string} event 事件名称
   * @param {...any} args 传递给订阅者的参数
   */
  publish(event, ...args) {
    if (!this.events[event]) {
      return;
    }
    this.events[event].forEach((callback) => {
      callback(...args);
    });
  }

  /**
   * 清除某个事件的所有订阅
   * @param {string} event 事件名称
   */
  clear(event) {
    if (this.events[event]) {
      delete this.events[event];
    }
  }

  /**
   * 获取某个事件的订阅者数量
   * @param {string} event 事件名称
   * @returns {number} 订阅者数量
   */
  getSubscriberCount(event) {
    return this.events[event] ? this.events[event].length : 0;
  }
}

手写 Ajax / 封装 Axios

手写 Ajax

  • 使用 XMLHttpRequest 创建请求。
  • 使用 Promise 包装异步操作,支持链式调用。
  • 支持 GET / POST(可拓展 PUT / DELETE 等)。
  • 简单处理响应状态码和异常。
function ajax({ url, method = 'GET', data = null }) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);

    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(xhr.responseText);
        } else {
          reject(new Error(xhr.statusText));
        }
      }
    };

    xhr.onerror = () => {
      reject(new Error('Network Error'));
    };

    if (method.toUpperCase() === 'POST' && data) {
      xhr.setRequestHeader('Content-Type', 'application/json');
      xhr.send(JSON.stringify(data));
    } else {
      xhr.send();
    }
  });
}

// 使用示例
// ajax({ url: '/api/test' })
//     .then(res => console.log(res))
//     .catch(err => console.error(err));

封装 Axios

  1. 类封装请求方法,统一接口调用。
  2. get/post 方法快捷封装,减少每次写 config
  3. 支持 JSON 请求 / 响应处理,可扩展 headers、拦截器等。
  4. Promise 链式调用,符合现代前端习惯。
class Axios {
  request(config) {
    const { url, method = 'GET', data = null } = config;
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open(method, url, true);

      xhr.onreadystatechange = () => {
        if (xhr.readyState === 4) {
          if (xhr.status >= 200 && xhr.status < 300) {
            try {
              resolve(JSON.parse(xhr.responseText));
            } catch (e) {
              resolve(xhr.responseText);
            }
          } else {
            reject(new Error(xhr.statusText));
          }
        }
      };

      xhr.onerror = () => reject(new Error('Network Error'));

      if (method.toUpperCase() === 'POST' && data) {
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify(data));
      } else {
        xhr.send();
      }
    });
  }

  get(url, config = {}) {
    return this.request({ ...config, url, method: 'GET' });
  }

  post(url, data, config = {}) {
    return this.request({ ...config, url, method: 'POST', data });
  }
}

// 使用示例
// const axios = new Axios();
// axios.get('/api/test').then(res => console.log(res));
// axios.post('/api/test', { name: 'V' }).then(res => console.log(res));

列表转树数据处理

假设从后端获取了大量的扁平化列表数据,请写一个函数将其转换为 ECharts tree 图所需要的嵌套结构。

输入数据:

const list = [
  { id: 1, name: '部门 A', parentId: 0 },
  { id: 2, name: '部门 B', parentId: 1 },
  { id: 3, name: '部门 C', parentId: 1 },
  { id: 4, name: '部门 D', parentId: 2 }
];

输出数据:

[{
    "id": 1,
    "name": "部门 A",
    "children": [
        { "id": 2, "name": "部门 B", "children": [...] },
        { "id": 3, "name": "部门 C", "children": [] }
    ]
}]

代码:

function buildTree(list) {
  const map = new Map();
  const result = [];

  // 1. 先把每个节点放入 map 中,并初始化 children
  list.forEach((item) => {
    map.set(item.id, { ...item, children: [] });
  });

  // 2. 构建父子关系
  list.forEach((item) => {
    if (item.parentId === 0) {
      // 根节点
      result.push(map.get(item.id));
    } else {
      const parent = map.get(item.parentId);
      if (parent) {
        parent.children.push(map.get(item.id));
      }
    }
  });

  return result;
}

函数柯里化实现

  • fn.length 获取函数形参个数,用于判断参数是否足够执行。
  • 每次调用返回新函数,把已有参数与新参数合并,递归调用。
  • 当参数足够时,调用原函数返回结果。
  • 支持多种组合形式调用:单参数、多参数、混合。
// 普通函数
function sum(a, b, c) {
  return a + b + c;
}

// 柯里化实现
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args); // 参数够了就执行
    } else {
      return (...rest) => curried(...args, ...rest); // 参数不够,返回新的函数
    }
  };
}

// 使用
const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2, 3)); // 6

sum 函数柯里化实现

设计一个 sum 函数,使其满足以下要求:

  • sum(1, 2).sumOf() // 返回 3
  • sum(1, 2)(3).sumOf() // 返回 6
  • sum(1)(2, 3, 4).sumOf() // 返回 10
  • sum(1, 2)(3, 4)(5).sumOf() // 返回 15
function sum(...args) {
  let total = args.reduce((acc, curr) => acc + curr, 0);

  function innerSum(...innerArgs) {
    total += innerArgs.reduce((acc, curr) => acc + curr, 0);
    return innerSum;
  }

  innerSum.sumOf = () => total;

  return innerSum;
}

实现 get 方法

var obj = { a: { b: { c: 2 } } };
console.log(get(obj, 'a.b.c')); // 输出 2

function get(obj, path) {
  // 使用 ?. 和 reduce 简化
  return path.split('.').reduce((acc, key) => acc?.[key], obj);
}

lastPromise 实现

单次请求控制:每次请求都会取消上一次的请求,只有最后一次请求会被执行。

function lastPromise(promiseFn) {
  let lastPromise = null;

  return function (...args) {
    const currentPromise = promiseFn(...args);
    lastPromise = currentPromise;

    return new Promise((resolve, reject) => {
      currentPromise
        .then((result) => {
          // 只有当当前 promise 是最后一个时,才 resolve
          if (currentPromise === lastPromise) {
            resolve(result);
          }
        })
        .catch((err) => {
          // 同样,只有最后一个 promise 的错误才会被抛出
          if (currentPromise === lastPromise) {
            reject(err);
          }
        });
    });
  };
}

// 使用
// const fetchData = lastPromise(async (url) => {
//     const res = await fetch(url);
//     return res.json();
// });

// fetchData('api/1');
// fetchData('api/2'); // 只有 'api/2' 的请求会被处理

promise 最大并发请求 / 调度器

  • 并发调度:控制最大并发数量,任务排队等候执行,保证不超过最大并发数量。
  • 链式调用:每个 add() 返回 Promise,保证异步任务执行顺序。
class Scheduler {
  constructor(limit) {
    this.limit = limit;
    this.queue = [];
    this.active = 0;
  }

  add(promiseFn) {
    return new Promise((resolve) => {
      const task = async () => {
        this.active++;
        try {
          const result = await promiseFn();
          resolve(result); // 将任务结果 resolve 出去
        } catch (e) {
          // 即使任务失败,也需要执行下一个
          resolve(); // 或者 reject(e),取决于业务需求
        } finally {
          this.active--;
          if (this.queue.length) {
            this.queue.shift()();
          }
        }
      };

      if (this.active < this.limit) {
        task();
      } else {
        this.queue.push(task);
      }
    });
  }
}

手写 promise.all 和 promise.race

Promise.all

收集每个 Promise 的结果,全部完成才 resolve,否则 reject

function myAll(promises) {
  return new Promise((resolve, reject) => {
    if (!promises || promises.length === 0) {
      return resolve([]);
    }

    let results = [],
      count = 0;
    promises.forEach((p, i) => {
      Promise.resolve(p)
        .then((val) => {
          results[i] = val;
          count++;
          if (count === promises.length) {
            resolve(results);
          }
        })
        .catch(reject);
    });
  });
}

Promise.race

谁先完成(resolve/reject)就返回结果。

function myRace(promises) {
  return new Promise((resolve, reject) => {
    if (!promises || promises.length === 0) {
      return; // 或者 resolve(undefined)
    }
    promises.forEach((p) => {
      Promise.resolve(p).then(resolve).catch(reject);
    });
  });
}

数组算法

数组去重

function unique(arr) {
  return [...new Set(arr)];
}

console.log(unique([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]

手写 flat 方法

Array.prototype.myFlat = function (depth = 1) {
  if (!Array.isArray(this)) {
    throw new TypeError('myFlat must be called on an array');
  }

  let result = [];
  for (const item of this) {
    if (Array.isArray(item) && depth > 0) {
      result.push(...item.myFlat(depth - 1));
    } else {
      result.push(item);
    }
  }
  return result;
};

数组扁平化

// 使用 reduce
function flatten(arr) {
  return arr.reduce(
    (acc, item) =>
      Array.isArray(item) ? acc.concat(flatten(item)) : acc.concat(item),
    []
  );
}

console.log(flatten([1, [2, [3, [4]]]])); // [1, 2, 3, 4]

// ES6 flat 方法
const arr = [1, [2, [3, [4, 5]]], 6];
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6]

排序算法

  • Array.prototype.sort():该方法可以对数组进行原地排序,即直接修改原数组,不会返回新的数组。默认情况下,它会将数组元素转换为字符串,然后按照 Unicode 码点排序。如果需要按照其他方式排序,可以传入一个比较函数作为参数。
  • Array.prototype.reverse():该方法可以将数组中的元素按照相反的顺序重新排列,并返回新的数组。
  • 冒泡排序(Bubble Sort):这是一种简单的排序算法,它重复地遍历要排序的数组,比较相邻的元素并交换位置,直到整个数组都已经排序。
  • 快速排序(Quick Sort):这是一种快速的排序算法,它的基本思想是选择一个基准元素,然后将数组中的元素分为小于基准元素和大于基准元素的两部分,再对这两部分分别进行排序。
  • 插入排序(Insertion Sort):这是一种简单的排序算法,它将数组分为已排序和未排序两部分,然后将未排序部分的第一个元素插入到已排序部分的正确位置上。
  • 选择排序(Selection Sort):这是一种简单的排序算法,它将数组分为已排序和未排序两部分,然后从未排序部分选择最小的元素并放到已排序部分的末尾。
  • 归并排序(Merge Sort):这是一种分治的排序算法,它将数组分成两个子数组,分别对这两个子数组进行排序,然后将排序后的子数组合并成一个有序的数组。

冒泡排序

function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let swapped = false;
    for (let j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换
        swapped = true;
      }
    }
    if (!swapped) break; // 如果一轮没有交换,说明已经有序
  }
  return arr;
}

console.log(bubbleSort([5, 2, 8, 3, 1])); // [1, 2, 3, 5, 8]

快速排序

  • 时间复杂度:O(n log n)
  • 如何对快排进行优化?
    • 优化基准:取中间值,三数取中或随机基准,降低最坏概率。
    • 小数组优化:当分区后子数组长度较小时,改用插入排序。
    • 尾递归优化:始终递归较小分区。
    • 三路划分:分为 < pivot= pivot> pivot 三段,避免重复比较。
    • 原地排序:尽量在原数组上交换,节省额外空间。
  • 快排最坏情况:每次选取的基准都是当前子数组的最大或最小元素,时间复杂度为 O(n^2)。
function quickSort(arr) {
  if (arr.length <= 1) return arr;
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = [];
  const right = [];
  const equal = [];

  for (let x of arr) {
    if (x < pivot) {
      left.push(x);
    } else if (x > pivot) {
      right.push(x);
    } else {
      equal.push(x);
    }
  }

  return [...quickSort(left), ...equal, ...quickSort(right)];
}

console.log(quickSort([5, 2, 8, 3, 1])); // [1, 2, 3, 5, 8]

归并排序

function mergeSort(arr) {
  // 递归终止条件
  if (arr.length <= 1) return arr;

  // 拆分数组
  const mid = Math.floor(arr.length / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);

  // 递归排序并合并
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const result = [];
  let i = 0,
    j = 0;

  // 比较两个子数组元素,按升序合并
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }

  // 拼接剩余元素
  return result.concat(left.slice(i)).concat(right.slice(j));
}

// 示例
console.log(mergeSort([5, 2, 9, 1, 3])); // [1, 2, 3, 5, 9]

数组操作函数

数组平分

fn([1,2,3,4,5], 2) //结果为 [[1,2],[3,4],[5]]

function splitArray(arr, size) {
  const result = [];
  for (let i = 0; i < arr.length; i += size) {
    result.push(arr.slice(i, i + size));
  }
  return result;
}

console.log(splitArray([1, 2, 3, 4, 5], 2)); // [[1, 2], [3, 4], [5]]

group 函数分类

function group(arr, fn) {
  return arr.reduce((acc, item) => {
    const key = fn(item);
    if (!acc[key]) acc[key] = [];
    acc[key].push(item);
    return acc;
  }, {});
}

const data = [6.1, 4.2, 6.3];
console.log(group(data, Math.floor)); // { 4: [4.2], 6: [6.1, 6.3] }

CSS 布局实现

CSS 三角形

.triangle {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid red;
}

可滚动列表前三个样式

.scrollable-list {
  max-height: 200px;
  overflow-y: auto;
}
.scrollable-list li:nth-child(-n + 3) {
  background-color: lightgreen;
}

响应式矩形布局

.rectangle {
  width: calc(100% - 100px); /* 距离左右各 50px */
  padding-bottom: 75%; /* 高度是宽度的 3/4,即 4:3 */
  background-color: lightblue;
  position: relative;
  margin: 0 auto; /* 水平居中 */
}

整理不易,有回答错误之处欢迎在评论区指正交流~