前端面试Javascript手撕题目回顾

在前端面试中,“手撕代码”环节是考察候选人编程能力和思维逻辑的重要部分。以下是一些常见的手撕代码题目及详细解答:

1. 实现数组扁平化(Flatten Array)

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

方法一:递归

function flatten(arr) {
  const result = [];
  arr.forEach(item => {
    if (Array.isArray(item)) {
      result.push(...flatten(item)); // 递归展开子数组
    } else {
      result.push(item);
    }
  });
  return result;
}

方法二:迭代 + 栈

function flatten(arr) {
  const result = [];
  const stack = [...arr]; // 使用栈模拟递归
  
  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      stack.push(...item); // 展开子数组并压入栈
    } else {
      result.unshift(item); // 保证顺序正确
    }
  }
  
  return result;
}

2. 手写 Promise

实现一个基本的Promise,包含thencatchfinally方法。

class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    
    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };
    
    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };
    
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
    
    return new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        try {
          const result = onFulfilled(this.value);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      };
      
      const handleRejected = () => {
        try {
          const result = onRejected(this.reason);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      };
      
      if (this.status === 'fulfilled') {
        setTimeout(handleFulfilled, 0);
      } else if (this.status === 'rejected') {
        setTimeout(handleRejected, 0);
      } else {
        this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0));
        this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0));
      }
    });
  }
  
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  
  finally(callback) {
    return this.then(
      value => MyPromise.resolve(callback()).then(() => value),
      reason => MyPromise.resolve(callback()).then(() => { throw reason; })
    );
  }
  
  static resolve(value) {
    if (value instanceof MyPromise) return 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 count = 0;
      
      if (promises.length === 0) {
        resolve(results);
        return;
      }
      
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          value => {
            results[index] = value;
            count++;
            if (count === 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)
        );
      });
    });
  }
}

3. 手写防抖(Debounce)和节流(Throttle)

防抖(Debounce):延迟执行,连续触发时重置计时器。

function debounce(func, delay, immediate = false) {
  let timer = null;
  
  return function(...args) {
    if (timer) clearTimeout(timer);
    
    if (immediate && !timer) {
      func.apply(this, args);
    }
    
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

节流(Throttle):固定频率执行,忽略中间触发。

function throttle(func, limit) {
  let inThrottle;
  let lastResult;
  
  return function(...args) {
    if (!inThrottle) {
      inThrottle = true;
      lastResult = func.apply(this, args);
      setTimeout(() => inThrottle = false, limit);
    }
    return lastResult;
  };
}

4. 手写深拷贝(Deep Clone)

递归复制对象,处理循环引用和特殊对象。

function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  
  // 处理循环引用
  if (map.has(obj)) return map.get(obj);
  
  // 处理特殊对象
  if (obj instanceof Date) return new Date(obj.getTime());
  if (obj instanceof RegExp) return new RegExp(obj);
  
  // 创建新对象
  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone);
  
  // 递归复制所有属性
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map);
    }
  }
  
  return clone;
}

5. 手写 call、apply 和 bind

call

Function.prototype.myCall = function(context = window, ...args) {
  const fn = Symbol('fn');
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

apply

Function.prototype.myApply = function(context = window, args = []) {
  const fn = Symbol('fn');
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

bind

Function.prototype.myBind = function(context = window, ...args) {
  const self = this;
  return function(...newArgs) {
    return self.apply(context, [...args, ...newArgs]);
  };
};

6. 手写 instanceof

判断对象是否是某个构造函数的实例。

function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  const prototype = Constructor.prototype;
  
  while (proto !== null) {
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  
  return false;
}

7. 实现观察者模式(发布-订阅)

class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }
  
  emit(eventName, ...args) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => callback(...args));
    }
  }
  
  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        cb => cb !== callback
      );
    }
  }
  
  once(eventName, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
  }
}

8. 实现简易版Vue响应式原理

基于Object.defineProperty()实现数据劫持。

class Vue {
  constructor(options) {
    this.$data = options.data;
    this.$options = options;
    
    // 数据响应化
    this.observe(this.$data);
    
    // 模拟编译过程
    new Compile(options.el, this);
  }
  
  observe(obj) {
    if (!obj || typeof obj !== 'object') return;
    
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
      
      // 代理data中的属性到Vue实例上
      this.proxyData(key);
    });
  }
  
  defineReactive(obj, key, val) {
    // 递归处理嵌套对象
    this.observe(val);
    
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 收集依赖
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (newVal === val) return;
        val = newVal;
        // 触发更新
        dep.notify();
      }
    });
  }
  
  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key];
      },
      set(newVal) {
        this.$data[key] = newVal;
      }
    });
  }
}

// 依赖收集器
class Dep {
  constructor() {
    this.subs = [];
  }
  
  addSub(watcher) {
    this.subs.push(watcher);
  }
  
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

// Watcher
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    
    // 触发getter,收集依赖
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }
  
  update() {
    this.cb.call(this.vm, this.vm[this.key]);
  }
}

// 模拟编译过程
class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    
    if (this.$el) {
      this.compile(this.$el);
    }
  }
  
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node);
      } else if (this.isTextNode(node)) {
        // 处理文本节点
        this.compileText(node);
      }
      
      // 递归处理子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }
  
  compileElement(node) {
    const attrs = node.attributes;
    Array.from(attrs).forEach(attr => {
      const attrName = attr.name;
      const exp = attr.value;
      
      if (this.isDirective(attrName)) {
        // 处理指令 v-xxx
        const dir = attrName.substring(2);
        this[dir] && this[dir](node, this.$vm, exp);
      } else if (this.isEventDirective(attrName)) {
        // 处理事件 @xxx
        const dir = attrName.substring(1);
        this.eventHandler(node, this.$vm, exp, dir);
      }
    });
  }
  
  compileText(node) {
    const text = node.textContent;
    const reg = /\{\{(.*)\}\}/;
    
    if (reg.test(text)) {
      this.text(node, this.$vm, RegExp.$1.trim());
    }
  }
  
  // 指令处理函数
  text(node, vm, exp) {
    this.update(node, vm, exp, 'text');
  }
  
  html(node, vm, exp) {
    this.update(node, vm, exp, 'html');
  }
  
  model(node, vm, exp) {
    this.update(node, vm, exp, 'model');
    
    // 双向绑定
    node.addEventListener('input', e => {
      vm[exp] = e.target.value;
    });
  }
  
  update(node, vm, exp, dir) {
    const updaterFn = this[dir + 'Updater'];
    updaterFn && updaterFn(node, vm[exp]);
    
    // 创建Watcher
    new Watcher(vm, exp, function(value) {
      updaterFn && updaterFn(node, value);
    });
  }
  
  textUpdater(node, value) {
    node.textContent = value;
  }
  
  htmlUpdater(node, value) {
    node.innerHTML = value;
  }
  
  modelUpdater(node, value) {
    node.value = value;
  }
  
  eventHandler(node, vm, exp, dir) {
    const fn = vm.$options.methods && vm.$options.methods[exp];
    if (dir && fn) {
      node.addEventListener(dir, fn.bind(vm));
    }
  }
  
  // 辅助方法
  isElementNode(node) {
    return node.nodeType === 1;
  }
  
  isTextNode(node) {
    return node.nodeType === 3;
  }
  
  isDirective(attr) {
    return attr.startsWith('v-');
  }
  
  isEventDirective(attr) {
    return attr.startsWith('@');
  }
}

9. 实现一个LRU缓存

基于双向链表和哈希表实现最近最少使用(LRU)缓存。

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
    this.head = new DLinkedNode();
    this.tail = new DLinkedNode();
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }
  
  get(key) {
    const node = this.cache.get(key);
    if (!node) return -1;
    
    // 移动到链表头部
    this.moveToHead(node);
    return node.value;
  }
  
  put(key, value) {
    const node = this.cache.get(key);
    
    if (node) {
      // 更新值并移动到头部
      node.value = value;
      this.moveToHead(node);
    } else {
      // 创建新节点
      const newNode = new DLinkedNode(key, value);
      this.cache.set(key, newNode);
      this.addToHead(newNode);
      
      if (this.cache.size > this.capacity) {
        // 超出容量,移除尾部节点
        const tailNode = this.removeTail();
        this.cache.delete(tailNode.key);
      }
    }
  }
  
  addToHead(node) {
    node.prev = this.head;
    node.next = this.head.next;
    this.head.next.prev = node;
    this.head.next = node;
  }
  
  removeNode(node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
  }
  
  moveToHead(node) {
    this.removeNode(node);
    this.addToHead(node);
  }
  
  removeTail() {
    const node = this.tail.prev;
    this.removeNode(node);
    return node;
  }
}

class DLinkedNode {
  constructor(key = 0, value = 0) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

10. 手写快排(Quick Sort)

基于分治法实现快速排序。

function quickSort(arr) {
  if (arr.length <= 1) return arr;
  
  const pivot = arr[0];
  const left = [];
  const right = [];
  
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] <= pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  
  return [...quickSort(left), pivot, ...quickSort(right)];
}

11. 实现数组去重

// 方法一:ES6 Set
function unique(arr) {
  return [...new Set(arr)];
}

// 方法二:双重循环
function unique(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (result.indexOf(arr[i]) === -1) {
      result.push(arr[i]);
    }
  }
  return result;
}

// 方法三:filter + indexOf
function unique(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}

12. 实现JSON.stringify

function jsonStringify(obj) {
  if (typeof obj === 'undefined' || typeof obj === 'function') {
    return undefined;
  }
  
  if (obj === null) {
    return 'null';
  }
  
  if (typeof obj === 'string') {
    return `"${obj}"`;
  }
  
  if (typeof obj === 'number' || typeof obj === 'boolean') {
    return String(obj);
  }
  
  if (obj instanceof Date) {
    return `"${obj.toISOString()}"`;
  }
  
  if (Array.isArray(obj)) {
    const elements = obj.map(item => {
      const value = jsonStringify(item);
      return value === undefined ? 'null' : value;
    });
    return `[${elements.join(',')}]`;
  }
  
  if (typeof obj === 'object') {
    const keys = Object.keys(obj);
    const properties = keys.map(key => {
      const value = jsonStringify(obj[key]);
      if (value !== undefined) {
        return `"${key}":${value}`;
      }
      return null;
    }).filter(Boolean);
    
    return `{${properties.join(',')}}`;
  }
  
  return String(obj);
}

13. 实现async/await

基于Generator和Promise实现async/await。

function run(gen) {
  const iterator = gen();
  
  function iterate(iteration) {
    if (iteration.done) return iteration.value;
    
    const promise = iteration.value;
    promise.then(result => {
      iterate(iterator.next(result));
    }).catch(error => {
      iterate(iterator.throw(error));
    });
  }
  
  return iterate(iterator.next());
}

// 使用示例
function fetchData() {
  return new Promise(resolve => setTimeout(() => resolve('data'), 1000));
}

const asyncFunction = run(function* () {
  try {
    const data = yield fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
});

总结

手撕代码题目通常考察基础编程能力、算法思维和对前端框架原理的理解。建议候选人:

  1. 熟练掌握JavaScript核心概念(原型链、闭包、异步编程等)。
  2. 理解常见设计模式(观察者模式、单例模式等)。
  3. 熟悉基本算法和数据结构(排序、链表、哈希表等)。
  4. 深入理解Vue/React等框架的核心原理。
  5. 注重代码的健壮性和边界条件处理。

你可能感兴趣的:(前端面试Javascript手撕题目回顾)