Skip to content

JavaScript 手写代码题

手写代码是前端面试的重要环节,考察对 JavaScript 核心概念的理解和编程能力。

🔧 工具函数实现

1. 防抖和节流

javascript
/**
 * 防抖函数 - 延迟执行,重复调用会重置计时器
 * @param {Function} func 要防抖的函数
 * @param {number} wait 延迟时间
 * @param {boolean} immediate 是否立即执行
 */
function debounce(func, wait, immediate = false) {
  let timeout
  
  return function executedFunction(...args) {
    const later = () => {
      timeout = null
      if (!immediate) func.apply(this, args)
    }
    
    const callNow = immediate && !timeout
    
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
    
    if (callNow) func.apply(this, args)
  }
}

/**
 * 节流函数 - 限制执行频率
 * @param {Function} func 要节流的函数
 * @param {number} limit 时间间隔
 */
function throttle(func, limit) {
  let inThrottle
  
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

// 使用示例
const debouncedSearch = debounce((query) => {
  console.log('搜索:', query)
}, 300)

const throttledScroll = throttle(() => {
  console.log('滚动事件')
}, 100)

2. 深拷贝

javascript
/**
 * 深拷贝函数 - 处理循环引用和各种数据类型
 * @param {*} obj 要拷贝的对象
 * @param {WeakMap} hash 用于处理循环引用
 */
function deepClone(obj, hash = new WeakMap()) {
  // null 或非对象类型直接返回
  if (obj === null || 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
}

// 测试用例
const original = {
  name: 'Alice',
  age: 25,
  hobbies: ['reading', 'coding'],
  address: {
    city: 'Beijing',
    country: 'China'
  },
  date: new Date(),
  regex: /test/g
}

// 添加循环引用
original.self = original

const cloned = deepClone(original)
console.log(cloned)
console.log(cloned !== original) // true
console.log(cloned.self === cloned) // true

3. 类型判断

javascript
/**
 * 精确的类型判断函数
 * @param {*} value 要判断的值
 * @returns {string} 类型字符串
 */
function getType(value) {
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
}

// 各种类型判断函数
const isArray = (value) => Array.isArray(value)
const isObject = (value) => getType(value) === 'object'
const isFunction = (value) => typeof value === 'function'
const isString = (value) => typeof value === 'string'
const isNumber = (value) => typeof value === 'number' && !isNaN(value)
const isBoolean = (value) => typeof value === 'boolean'
const isNull = (value) => value === null
const isUndefined = (value) => value === undefined
const isDate = (value) => getType(value) === 'date'
const isRegExp = (value) => getType(value) === 'regexp'

// 判断空对象
function isEmptyObject(obj) {
  if (!isObject(obj)) return false
  return Object.keys(obj).length === 0
}

// 判断空值
function isEmpty(value) {
  if (isNull(value) || isUndefined(value)) return true
  if (isString(value) || isArray(value)) return value.length === 0
  if (isObject(value)) return Object.keys(value).length === 0
  return false
}

// 测试
console.log(getType([])) // 'array'
console.log(getType({})) // 'object'
console.log(getType(null)) // 'null'
console.log(getType(new Date())) // 'date'

🎯 Promise 相关实现

1. 手写 Promise

javascript
class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []
    
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
        this.onFulfilledCallbacks.forEach(fn => fn())
      }
    }
    
    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = '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 : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
    
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value)
            this.resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      }
      
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason)
            this.resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      }
      
      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value)
              this.resolvePromise(promise2, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0)
        })
        
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason)
              this.resolvePromise(promise2, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0)
        })
      }
    })
    
    return promise2
  }
  
  resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise'))
    }
    
    if (x instanceof MyPromise) {
      x.then(resolve, reject)
    } else {
      resolve(x)
    }
  }
  
  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) {
    return new MyPromise((resolve) => {
      resolve(value)
    })
  }
  
  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason)
    })
  }
  
  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = []
      let completedCount = 0
      
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          value => {
            results[index] = value
            completedCount++
            if (completedCount === promises.length) {
              resolve(results)
            }
          },
          reject
        )
      })
    })
  }
  
  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(promise => {
        MyPromise.resolve(promise).then(resolve, reject)
      })
    })
  }
}

// 测试
const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('成功'), 1000)
})

promise.then(value => {
  console.log(value) // '成功'
  return '链式调用'
}).then(value => {
  console.log(value) // '链式调用'
})

2. Promise 工具函数

javascript
/**
 * Promise.allSettled 实现
 * 等待所有 Promise 完成,无论成功还是失败
 */
function allSettled(promises) {
  return Promise.all(
    promises.map(promise =>
      Promise.resolve(promise)
        .then(value => ({ status: 'fulfilled', value }))
        .catch(reason => ({ status: 'rejected', reason }))
    )
  )
}

/**
 * Promise 重试函数
 * @param {Function} fn 要重试的异步函数
 * @param {number} maxRetries 最大重试次数
 * @param {number} delay 重试间隔
 */
async function retry(fn, maxRetries = 3, delay = 1000) {
  let lastError
  
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      if (i < maxRetries) {
        await new Promise(resolve => setTimeout(resolve, delay))
      }
    }
  }
  
  throw lastError
}

/**
 * Promise 超时函数
 * @param {Promise} promise 原始 Promise
 * @param {number} timeout 超时时间
 */
function withTimeout(promise, timeout) {
  return Promise.race([
    promise,
    new Promise((_, reject) => {
      setTimeout(() => reject(new Error('Promise timeout')), timeout)
    })
  ])
}

// 使用示例
async function fetchWithRetry() {
  return retry(
    () => fetch('/api/data').then(res => res.json()),
    3,
    1000
  )
}

async function fetchWithTimeout() {
  return withTimeout(
    fetch('/api/data'),
    5000
  )
}

🔄 函数式编程

1. 柯里化

javascript
/**
 * 柯里化函数
 * @param {Function} fn 要柯里化的函数
 * @returns {Function} 柯里化后的函数
 */
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function(...nextArgs) {
        return curried.apply(this, args.concat(nextArgs))
      }
    }
  }
}

// 示例函数
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

// 实际应用
const multiply = curry((a, b, c) => a * b * c)
const multiplyByTwo = multiply(2)
const multiplyByTwoAndThree = multiplyByTwo(3)

console.log(multiplyByTwoAndThree(4)) // 24

2. 函数组合

javascript
/**
 * 函数组合 - 从右到左执行
 * @param {...Function} fns 要组合的函数
 */
function compose(...fns) {
  return function(value) {
    return fns.reduceRight((acc, fn) => fn(acc), value)
  }
}

/**
 * 管道函数 - 从左到右执行
 * @param {...Function} fns 要组合的函数
 */
function pipe(...fns) {
  return function(value) {
    return fns.reduce((acc, fn) => fn(acc), value)
  }
}

// 示例函数
const add1 = x => x + 1
const multiply2 = x => x * 2
const square = x => x * x

// 使用组合
const composedFn = compose(square, multiply2, add1)
console.log(composedFn(3)) // ((3 + 1) * 2)² = 64

// 使用管道
const pipedFn = pipe(add1, multiply2, square)
console.log(pipedFn(3)) // ((3 + 1) * 2)² = 64

🎨 数组方法实现

1. 手写 map、filter、reduce

javascript
// 手写 map
Array.prototype.myMap = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('Callback must be a function')
  }
  
  const result = []
  for (let i = 0; i < this.length; i++) {
    if (i in this) {
      result[i] = callback.call(thisArg, this[i], i, this)
    }
  }
  return result
}

// 手写 filter
Array.prototype.myFilter = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('Callback must be a function')
  }
  
  const result = []
  for (let i = 0; i < this.length; i++) {
    if (i in this && callback.call(thisArg, this[i], i, this)) {
      result.push(this[i])
    }
  }
  return result
}

// 手写 reduce
Array.prototype.myReduce = function(callback, initialValue) {
  if (typeof callback !== 'function') {
    throw new TypeError('Callback must be a function')
  }
  
  let accumulator = initialValue
  let startIndex = 0
  
  // 如果没有提供初始值,使用数组第一个元素
  if (initialValue === undefined) {
    if (this.length === 0) {
      throw new TypeError('Reduce of empty array with no initial value')
    }
    accumulator = this[0]
    startIndex = 1
  }
  
  for (let i = startIndex; i < this.length; i++) {
    if (i in this) {
      accumulator = callback(accumulator, this[i], i, this)
    }
  }
  
  return accumulator
}

// 测试
const numbers = [1, 2, 3, 4, 5]

console.log(numbers.myMap(x => x * 2)) // [2, 4, 6, 8, 10]
console.log(numbers.myFilter(x => x % 2 === 0)) // [2, 4]
console.log(numbers.myReduce((acc, x) => acc + x, 0)) // 15

2. 数组扁平化

javascript
/**
 * 数组扁平化 - 递归实现
 * @param {Array} arr 要扁平化的数组
 * @param {number} depth 扁平化深度
 */
function flatten(arr, depth = 1) {
  if (depth <= 0) return arr.slice()
  
  return arr.reduce((acc, val) => {
    if (Array.isArray(val)) {
      acc.push(...flatten(val, depth - 1))
    } else {
      acc.push(val)
    }
    return acc
  }, [])
}

/**
 * 完全扁平化
 * @param {Array} arr 要扁平化的数组
 */
function flattenDeep(arr) {
  return arr.reduce((acc, val) => {
    return acc.concat(Array.isArray(val) ? flattenDeep(val) : val)
  }, [])
}

/**
 * 使用栈实现扁平化
 * @param {Array} arr 要扁平化的数组
 */
function flattenWithStack(arr) {
  const result = []
  const stack = [...arr]
  
  while (stack.length) {
    const next = stack.pop()
    if (Array.isArray(next)) {
      stack.push(...next)
    } else {
      result.push(next)
    }
  }
  
  return result.reverse()
}

// 测试
const nestedArray = [1, [2, 3], [4, [5, 6]], [[7, 8], 9]]

console.log(flatten(nestedArray, 1)) // [1, 2, 3, 4, [5, 6], [7, 8], 9]
console.log(flatten(nestedArray, 2)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(flattenDeep(nestedArray)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]