手撕

26 阅读4分钟

1. 使用闭包实现计数器

    const counter = (() => {
        let n = 0;
        return () => ++n;
    })();

2. 实现数组转树

function arrayToTree(array, options = {}) {
  // 配置默认值
  const {
    id = 'id',
    parentId = 'parentId',
    rootValue = null
  } = options;

  // 创建ID到节点的映射(优化查找性能)
  const nodeMap = new Map();
  array.forEach(item => {
    nodeMap.set(item[id], { ...item, children: [] });
  });

  // 构建树结构
  const tree = [];
  array.forEach(item => {
    const currentNode = nodeMap.get(item[id]);
    const parentNode = nodeMap.get(item[parentId]);

    if (parentNode) {
      // 有父节点,添加到父节点的children中
      parentNode.children.push(currentNode);
    } else if (item[parentId] === rootValue) {
      // 无父节点且符合根节点条件,直接加入树
      tree.push(currentNode);
    }
  });

  return tree;
}

  1. 实现千分位
// 整数部分添加千分位
  const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  1. 实现 call apply bind
// 1. 实现call方法
Function.prototype.myCall = function(context, ...args) {
  // 处理context为null/undefined的情况,默认指向window
  context = context || window;
  
  // 创建一个唯一的属性名,避免覆盖context原有的属性
  const fnKey = Symbol('fn');
  
  // 将当前函数(this)赋值给context的临时属性
  context[fnKey] = this;
  
  // 调用函数,传入参数
  const result = context[fnKey](...args);
  
  // 删除临时属性,避免污染context
  delete context[fnKey];
  
  // 返回函数执行结果
  return result;
};

// 2. 实现apply方法(与call的区别是参数以数组形式传入)
Function.prototype.myApply = function(context, args = []) {
  context = context || window;
  const fnKey = Symbol('fn');
  
  context[fnKey] = this;
  
  // 注意:args需要确保是数组,这里用扩展运算符传入
  const result = context[fnKey](...args);
  
  delete context[fnKey];
  return result;
};

// 3. 实现bind方法(返回一个绑定了this的新函数,支持柯里化)
Function.prototype.myBind = function(context, ...args1) {
  const self = this; // 保存原函数
  
  // 返回一个新函数
  function boundFn(...args2) {
    // 合并两次传入的参数
    const totalArgs = [...args1, ...args2];
    
    // 注意:如果是通过new调用boundFn,this应该指向实例对象
    if (this instanceof boundFn) {
      // 用原函数作为构造函数创建实例
      return new self(...totalArgs);
    }
    
    // 普通调用,用call改变this指向
    return self.myCall(context, ...totalArgs);
  }
  
  // 维护原型链关系
  boundFn.prototype = Object.create(self.prototype);
  boundFn.prototype.constructor = boundFn;
  
  return boundFn;
};

// 测试用例
const obj = { name: 'Alice' };

function testFn(age, hobby) {
  console.log(`Name: ${this.name}, Age: ${age}, Hobby: ${hobby}`);
  return { name: this.name, age, hobby };
}

// 测试call
testFn.myCall(obj, 20, 'reading'); // Name: Alice, Age: 20, Hobby: reading

// 测试apply
testFn.myApply(obj, [25, 'sports']); // Name: Alice, Age: 25, Hobby: sports

// 测试bind
const boundFn = testFn.myBind(obj, 30);
boundFn('coding'); // Name: Alice, Age: 30, Hobby: coding

// 测试bind的构造函数用法
const instance = new boundFn('singing');
console.log(instance); // { name: undefined, age: 30, hobby: 'singing' }(this指向实例)
  1. 最长公共子序列
/**
 * 计算两个字符串的最长公共子序列长度
 * @param {string} str1 - 第一个字符串
 * @param {string} str2 - 第二个字符串
 * @returns {number} 最长公共子序列的长度
 */
function lcsLength(str1, str2) {
  const m = str1.length;
  const n = str2.length;
  
  // 创建二维数组dp,dp[i][j]表示str1[0..i-1]和str2[0..j-1]的LCS长度
  const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
  
  // 填充dp数组
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        // 当前字符相同,LCS长度为左上角值+1
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        // 当前字符不同,取上方或左方的最大值
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  
  return dp[m][n];
}
  1. 函数柯里化
/**
 * 通用的函数柯里化工具
 * @param {Function} fn - 需要柯里化的函数
 * @param {Array} args - 已接收的参数(用于递归传递)
 * @returns {Function} 柯里化后的函数
 */
function curry(fn, args = []) {
  // 获取原函数的参数数量
  const arity = fn.length;
  
  // 返回一个新函数接收剩余参数
  return function(...rest) {
    // 合并已接收的参数和新参数
    const newArgs = [...args, ...rest];
    
    // 如果参数数量足够,执行原函数;否则继续柯里化
    if (newArgs.length >= arity) {
      return fn.apply(this, newArgs);
    } else {
      return curry(fn, newArgs);
    }
  };
}
  1. 实现LRU缓存
/**
 * LRU缓存实现
 * @param {number} capacity - 缓存最大容量
 */
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    // 使用Map存储缓存,Map的迭代顺序是插入顺序,可利用此特性维护访问顺序
    this.cache = new Map();
  }

  /**
   * 获取缓存值
   * @param {*} key - 缓存键
   * @returns {*} 缓存值,若不存在则返回-1
   */
  get(key) {
    // 缓存中不存在该键
    if (!this.cache.has(key)) {
      return -1;
    }

    // 存在则先删除再重新插入,保证最近访问的键在最后
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    
    return value;
  }

  /**
   * 设置缓存值
   * @param {*} key - 缓存键
   * @param {*} value - 缓存值
   */
  put(key, value) {
    // 如果键已存在,先删除(后续会重新插入)
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } 
    // 如果缓存已满,删除最久未使用的键(Map的第一个元素)
    else if (this.cache.size >= this.capacity) {
      // 获取Map的第一个键并删除
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }

    // 插入新值(最新访问的键会放在最后)
    this.cache.set(key, value);
  }
}

// 测试用例
const lruCache = new LRUCache(2);

lruCache.put(1, 1); // 缓存: {1 => 1}
lruCache.put(2, 2); // 缓存: {1 => 1, 2 => 2}
console.log(lruCache.get(1));    // 返回 1,缓存变为 {2 => 2, 1 => 1}(1被访问,移到最后)
lruCache.put(3, 3); // 容量已满,删除最久未使用的2,缓存: {1 => 1, 3 => 3}
console.log(lruCache.get(2));    // 返回 -1(2已被淘汰)
lruCache.put(4, 4); // 容量已满,删除最久未使用的1,缓存: {3 => 3, 4 => 4}
console.log(lruCache.get(1));    // 返回 -1(1已被淘汰)
console.log(lruCache.get(3));    // 返回 3,缓存变为 {4 => 4, 3 => 3}
console.log(lruCache.get(4));    // 返回 4,缓存变为 {3 => 3, 4 => 4}