JavaScript手写题面试题大全

52 阅读7分钟

本文档整理了前端面试中常见的手写代码题目,包含完整的实现代码、详细说明和测试用例。

数组扁平化

将多维数组转换为一维数组。

方法一:外部函数+内部递归辅助函数

function flattenArray(arr) {
  const result = [];
  
  function flatten(element) {
    if (!Array.isArray(element)) {
      result.push(element);
    } else {
      for (let i = 0; i < element.length; i++) {
        flatten(element[i]);
      }
    }
  }
  
  flatten(arr);
  return result;
}

优点:性能较好,只创建一个结果数组 缺点:代码稍长,使用了嵌套函数

方法二:使用reduce和递归

function flattenArrayES6(arr) {
  return arr.reduce((acc, curr) => {
    return acc.concat(Array.isArray(curr) ? flattenArrayES6(curr) : curr);
  }, []);
}

优点:代码简洁,符合函数式编程风格 缺点:每次递归都会创建新数组,性能较差

方法三:直接递归+concat

function flatten(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    if (Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(item);
    }
  }
  return result;
}

优点:实现直观,逻辑清晰 缺点:每次递归调用concat都会创建新数组

测试用例

const nestedArray = [1, [2, [3, 4, 5]], 6, [7]];
console.log('原始数组:', nestedArray);
console.log('方法1结果:', flattenArray(nestedArray));
console.log('方法2结果:', flattenArrayES6(nestedArray));
console.log('方法3结果:', flatten(nestedArray));

性能对比:方法1 > 方法3 > 方法2 代码简洁度:方法2 > 方法3 > 方法1


函数节流和防抖

函数节流 (Throttle)

在指定时间内,无论调用多少次,函数最多执行一次。

function throttle(fn, delay) {
  let lastTime = 0;
  
  return function(...args) {
    const now = Date.now();
    
    if (now - lastTime >= delay) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

应用场景:滚动事件、窗口调整、鼠标移动等高频触发的事件

函数防抖 (Debounce)

在连续多次调用后,只执行最后一次调用或第一次调用。

function debounce(fn, delay, immediate = false) {
  let timeout = null;
  
  return function(...args) {
    const context = this;
    
    if (timeout) clearTimeout(timeout);
    
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(() => {
        timeout = null;
      }, delay);
      
      if (callNow) fn.apply(context, args);
    } else {
      timeout = setTimeout(() => {
        fn.apply(context, args);
      }, delay);
    }
  };
}

应用场景:搜索输入、表单验证、窗口调整完成后执行操作

加强版节流

结合节流和防抖的特性,既保证立即响应,又限制执行频率。

function throttleEnhanced(fn, delay) {
  let lastTime = 0;
  let timeout = null;
  
  return function(...args) {
    const now = Date.now();
    const remaining = delay - (now - lastTime);
    const context = this;
    
    if (timeout) clearTimeout(timeout);
    
    if (remaining <= 0) {
      fn.apply(context, args);
      lastTime = now;
    } else {
      timeout = setTimeout(() => {
        fn.apply(context, args);
        lastTime = Date.now();
      }, remaining);
    }
  };
}

测试用例

function testFunction(type) {
  console.log(`${type} 函数执行了,时间: ${new Date().toLocaleTimeString()}`);
}

// 节流测试
const throttledFn = throttle(() => testFunction('节流'), 1000);

// 防抖测试(非立即执行)
const debouncedFn = debounce(() => testFunction('防抖(非立即)'), 1000);

// 防抖测试(立即执行)
const debouncedImmediateFn = debounce(() => testFunction('防抖(立即)'), 1000, true);

// 加强版节流测试
const enhancedThrottledFn = throttleEnhanced(() => testFunction('加强版节流'), 1000);

手写Promise

符合Promise/A+规范的简化版本实现。Promise是一种异步编程解决方案,用于处理异步操作的结果。

基础实现

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onFulfilledCallbacks.forEach(callback => callback());
      }
    };
    
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(callback => callback());
      }
    };
    
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
    
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }
      
      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }
      
      if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
        
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });
    
    return promise2;
  }
  
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  
  finally(onFinally) {
    return this.then(
      value => MyPromise.resolve(onFinally()).then(() => value),
      reason => MyPromise.resolve(onFinally()).then(() => { throw reason; })
    );
  }
}

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  
  let called = false;
  
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      called = true;
      reject(error);
    }
  } else {
    resolve(x);
  }
}

静态方法

Promise的静态方法用于处理多个Promise实例,提供不同的组合策略。

// 静态resolve方法 - 创建一个已解析的Promise
static resolve(value) {
  if (value instanceof MyPromise) {
    return value;
  }
  return new MyPromise((resolve, reject) => {
    resolve(value);
  });
}

// 静态reject方法 - 创建一个已拒绝的Promise
static reject(reason) {
  return new MyPromise((resolve, reject) => {
    reject(reason);
  });
}

// 静态all方法 - 所有Promise都成功时成功,任何一个失败时立即失败
static all(promises) {
  return new MyPromise((resolve, reject) => {
    const results = [];
    let count = 0;
    
    if (promises.length === 0) {
      resolve(results);
      return;
    }
    
    for (let i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(
        value => {
          results[i] = value;
          count++;
          if (count === promises.length) {
            resolve(results);
          }
        },
        reason => {
          reject(reason);
        }
      );
    }
  });
}

// 静态race方法 - 返回第一个完成的Promise的结果
static race(promises) {
  return new MyPromise((resolve, reject) => {
    for (let i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(
        value => resolve(value),
        reason => reject(reason)
      );
    }
  });
}

// 静态allSettled方法(ES2020) - 等待所有Promise完成,无论成功或失败
static allSettled(promises) {
  return new MyPromise((resolve) => {
    const results = [];
    let count = 0;
    
    if (promises.length === 0) {
      resolve(results);
      return;
    }
    
    for (let i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(
        value => {
          results[i] = { status: FULFILLED, value };
          count++;
          if (count === promises.length) {
            resolve(results);
          }
        },
        reason => {
          results[i] = { status: REJECTED, reason };
          count++;
          if (count === promises.length) {
            resolve(results);
          }
        }
      );
    }
  });
}

// 静态any方法(ES2021) - 只要有一个Promise成功就成功,所有都失败才失败
static any(promises) {
  return new MyPromise((resolve, reject) => {
    const errors = [];
    let count = 0;
    
    if (promises.length === 0) {
      reject(new AggregateError([], 'All promises were rejected'));
      return;
    }
    
    for (let i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(
        value => {
          resolve(value);
        },
        reason => {
          errors[i] = reason;
          count++;
          if (count === promises.length) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        }
      );
    }
  });
}

静态方法对比

方法成功条件失败条件返回值
Promise.all()所有Promise都成功任何一个Promise失败成功值的数组
Promise.race()第一个完成的Promise第一个完成的Promise第一个完成的结果
Promise.allSettled()所有Promise都完成不会失败包含所有结果的数组
Promise.any()至少一个Promise成功所有Promise都失败第一个成功的结果
Promise.resolve()总是成功不会失败包装后的值
Promise.reject()不会成功总是失败包装后的错误

DOM渲染

将虚拟DOM渲染为真实DOM。

function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {
    vnode = String(vnode);
  }
  
  // 字符串类型直接就是文本节点
  if (typeof vnode === "string") {
    try {
      const textNode = document.createTextNode(vnode);
      return textNode;
    } catch (e) {
      console.warn('DOM渲染函数需要在浏览器环境下运行');
      return { type: 'text', content: vnode };
    }
  }
  
  // 普通DOM元素
  try {
    const element = document.createElement(vnode.tag);
    
    // 添加属性
    if (vnode.attrs) {
      for (let key in vnode.attrs) {
        const value = vnode.attrs[key];
        element.setAttribute(key, value);
      }
    }
    
    // 处理子节点
    if (vnode.children && vnode.children.length) {
      vnode.children.forEach(child => {
        const childElement = _render(child);
        element.appendChild(childElement);
      });
    }
    
    return element;
  } catch (e) {
    console.warn('DOM渲染函数需要在浏览器环境下运行');
    return {
      type: 'element',
      tag: vnode.tag,
      attrs: vnode.attrs,
      children: vnode.children ? vnode.children.map(child => _render(child)) : []
    };
  }
}

测试用例

const vDom = {
  tag: 'DIV',
  attrs: {
    id: 'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
};

// 在浏览器环境下测试
const result = _render(vDom);
console.log('渲染结果:', result);

树结构转换

树转数组

function treeToArray(tree) {
  let result = [];
  let queue = [];
  
  result.push({ id: tree.id, name: tree.name, parentId: 0 });
  queue.unshift({ id: tree.id, name: tree.name, children: tree.children });
  
  while (queue.length > 0) {
    const currentNode = queue.shift();
    if (currentNode.children && currentNode.children.length > 0) {
      for (let i = 0; i < currentNode.children.length; i++) {
        const childNode = currentNode.children[i];
        const { id, name } = childNode;
        queue.unshift(childNode);
        result.push({
          id,
          name,
          parentId: currentNode.id
        });
      }
    }
  }
  return result;
}

数组转树

function arrayToTree(arr) {
  let map = new Map();
  let result = null;
  
  for (let i = 0; i < arr.length; i++) {
    const { id, name } = arr[i];
    const children = [];
    map.set(id, { id, name, children });
  }

  for (let i = 0; i < arr.length; i++) {
    const { parentId, id } = arr[i];
    if (parentId !== 0) {
      const parentNode = map.get(parentId);
      parentNode.children.push(map.get(id));
    } else {
      result = map.get(id);
    }
  }
  return result;
}

测试用例

const testTree = {
  id: 1,
  name: "部门A",
  children: [
    {
      id: 2,
      name: "部门B",
      children: [
        {
          id: 4,
          name: "部门D",
          children: [
            { id: 7, name: "部门G", children: [] },
            { id: 8, name: "部门H", children: [] }
          ]
        },
        { id: 5, name: "部门E", children: [] }
      ]
    },
    {
      id: 3,
      name: "部门C",
      children: [
        { id: 6, name: "部门F", children: [] }
      ]
    }
  ]
};

const testArray = [
  { 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: 5, name: '部门E', parentId: 2 },
  { id: 6, name: '部门F', parentId: 3 },
  { id: 7, name: '部门G', parentId: 4 },
  { id: 8, name: '部门H', parentId: 4 }
];

console.log('树转数组结果:', treeToArray(testTree));
console.log('数组转树结果:', arrayToTree(testArray));

版本比较

function compareVersion(arr) {
  let copyArr = [...arr];
  copyArr.sort((a, b) => {
    const tempA = a.split('.');
    const tempB = b.split('.');
    const maxLen = Math.max(tempA.length, tempB.length);
    
    for (let i = 0; i < maxLen; i++) {
      const targetA = +tempA[i] || 0;
      const targetB = +tempB[i] || 0;
      if (targetA === targetB) continue;
      return targetA - targetB;
    }
    return 0;
  });
  return copyArr;
}

// 测试
const versions = ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'];
console.log('版本比较结果:', compareVersion(versions));

深拷贝

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return null;
  if (typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  if (hash.has(obj)) return hash.get(obj);
  
  const cloneObj = new obj.constructor();
  hash.set(obj, cloneObj);
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  
  return cloneObj;
}

函数柯里化

function curry(fn) {
  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));
      };
    }
  };
}

// 使用示例
function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6