手写题总结

数组map手写

  • map 是数组的遍历方法,会创建新的新的数组并且实现返回讷!新数组的每个元素是原数组在经过回调函数处理后的操作吧,不会修改原数组讷!无副作用讷

  • 核心原理

    • 遍历数组的每一个元素

    • 对每个元素进行传入回调函数进行处理后,返回值加入新数组的操作吧

    • 核心的特性:不修改原数组、返回新数组、遍历所有的元素

Array.prototype.myMap = function (callback,  thisArgs) {
    // 进行边界判断
    if (!this || typeof callback !== "function") {
        throw new TypeError("Array.prototype.myMap: callback is not a function");
    }

    const arr = Object(this);
    const len = arr.length;
    const res = [];

    // 开始执行真真的操作实现吧
    for (let i = 0; i < len; i++) {
        if (i in arr) {
            const val = callback.call(thisArgs, arr[i], i, arr);
            res.push(val);
        }
    }

    return res;
}

数组filter手写

Array.prototype.myFilter = function (callback, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError('Cannot read property "myFilter" of null or undefined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }

  const arr = Object(this);
  const len = arr.length >>> 0;
  const result = [];

  for (let i = 0; i < len; i++) {
    if (i in arr) {
      // 执行回调,判断是否满足条件
      const isPass = callback.call(thisArg, arr[i], i, arr);
      if (isPass) {
        result.push(arr[i]); // 满足条件则加入结果
      }
    }
  }

  return result;
};

数组reduce手写

reduce 是数组归并方法,将数组元素从左到右遍历,通过回调函数累积为单个值返回,可实现求和、分组、扁平化等多种功能,灵活性极强

  • 接收两个核心参数:回调函数、初始值;

  • 回调函数接收「累计值、当前元素、索引、原数组」四个参数;

  • 若传初始值:累计值初始为该值,从第一个元素开始遍历;

  • 若不传初始值:累计值初始为第一个元素,从第二个元素开始遍历;

  • 核心:通过回调函数不断更新累计值,最终返回累计值

Array.prototype.myReduce = function (callback, initialValue) {
    const arr = Object(this);
    const len = arr.length;
    
    // 累计器的定义吧
    let accumulator;
    let startIndex = 0;

    if (initialValue !== undefined) {
        accumulator = initialValue;
    } else {
        if (len === 0) {
            throw new TypeError("Reduce.prototype.reduceReduce: Array of size zero with no initial value or initial value is undefined");
        }
        // 核心是为了跳过原型链上的一些属性吧
        while (startIndex < len && !(startIndex in arr)) {
            startIndex++;
        }
        accumulator = arr[startIndex];
        startIndex++;
    }

    // 开始执行真真的操作实现吧
    for (let i = startIndex; i < len; i++) {
        if (i in arr) {
            const val = callback.call(this, accumulator, arr[i], i, arr);
            accumulator = val;
        }
    }
    return accumulator;
}

数组flat手写

Array.prototype.myFlat = function (depth = 1) {
  if (depth < 1) return [...this]; // 深度≤0,返回原数组拷贝

  const result = [];
  const arr = Object(this);
  const len = arr.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in arr) {
      const item = arr[i];
      // 判断是否为数组(兼容跨iframe场景)
      if (Array.isArray(item)) {
        // 递归拍平,深度-1
        const flatItem = item.myFlat(depth - 1);
        result.push(...flatItem);
      } else {
        result.push(item);
      }
    }
  }

  return result;
};

Array.prototype.myFlatNonRecursive = function (depth = 1) {
  if (depth < 1) return [...this];

  let arr = [...this]; // 拷贝原数组,避免修改原数组
  let currentDepth = 0;

  // 循环处理,直到深度达标或无嵌套数组
  while (currentDepth < depth && arr.some(item => Array.isArray(item))) {
    // 使用 reduce 替代扩展运算符,避免参数过多的问题
    arr = arr.reduce((acc, item) => {
      return acc.concat(Array.isArray(item) ? item : [item]);
    }, []);
    currentDepth++;
  }

  return arr;
};

防抖/节流

  • 防抖

    • 防抖是触发事件后,延迟n秒后执行回调,若在n秒内再次触发,重置延迟时间

function debounce(fn, delay = 300, immediate = false) {
    let timer = null;

    const debounced = function (...args) {
        const context = this;

        // 每次触发之前进行重置一下定时器吧
        if (timer) {
            clearTimeout(timer);
        }

        // 如果是立即执行函数的话
        if (immediate) {
            // 定时器存在的的话,说明是第一次触发的讷,此时就需要立即执行一下吧
            const callNow = !timer;
            timer = setTimeout(() => {
                timer = null;
            }, delay);
            if (callNow) {
                fn.apply(context, args);
            }
        } else {
            // 如果不是立即执行函数的话,那么就需要等定时器到时间了
            timer = setTimeout(() => {
                fn.apply(context, args);
                timer = null;
            }, delay);
        }
    }

    debounced.cancel = function () {
        if (timer) {
            clearTimeout(timer);
        }
    }

    return debounced;
}
  • 节流:触发事件后,每隔 n 秒只能执行一次回调。核心是「固定频率执行」,不管触发多频繁

function throttle(fn, interval = 100) {
    let lastTime = 0;  // 记录一下上一次执行的时间
    let timer = null;  // 记录一下定时器讷

    const throttled = function (...args) {
        const context = this;
        const now = Date.now(); // 获取得到当前的时间戳讷

        // 计算一下本次的剩余时间吧
        const remaining = interval - (now - lastTime);

        // 如果剩余时间小于0的话
        if (remaining <= 0) {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            lastTime = now;
            fn.apply(context, args);
        } else if (!timer) {
            // 剩余时间>0,设置定时器,延迟执行一下吧
            timer = setTimeout(() => {
                fn.apply(context, args);
                lastTime = Date.now();
                timer = null;
            }, remaining);
        }
    }

    throttled.cancel = function () {
        if (timer) {
            clearTimeout(timer);
        }
    }

    return throttled;
}
  • 节流:控制“执行频率”(单位时间最多执行一次)。

  • 防抖:控制“执行时机”(等待触发停止后执行一次)。

  • 节流是“稀释”频率,防抖是“合并”触发

基础版本

  • 好理解很多讷

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

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

深拷贝函数

  • 核心的原理是常见出一个新对象/数组,递归的进行复制一份原数据进入新对象即可吧

  • 原理是:

    • 区别数据类型:基本数据类型直接赋值即可,应用数据类型进行递归的处理一下吧

    • 处理特殊的数据类型:Date/RegExp/函数/Map/Set

    • 解决循环引用问题即可:weakmap 就可以实现这个功能吧

    • 核心:递归思想 + 类型判断的解决吧 + 循环引用问题的解决实现吧

function deepClone(target, hash = new WeakMap()) {
    // 首先处理一下基础数据类型和 null 和 undefined 的特殊情况
    if (target === null || typeof target !== "object") {
        return target;
    }

    // 提前检查一下是否具备循环引用的情况出现吧
    // 因为基础数据类型和特殊的数据类型处理完了就是处理引用数据类型了讷
    if (hash.has(target)) {
        return hash.get(target);
    }

    let res;
    // 开始实现处理一下引用数据类型的情况吧
    /**
     * Date
     * RegExp
     * Map
     * Set
     * Function
     * Object | Array 的情况吧
     */
    if (target instanceof Date) {
        res = new Date(target);
        hash.set(target, res);
        return res;
    }

    /**
     * 正则表达式的话也是一样的讷
     */
    if (target instanceof RegExp) {
        res = new RegExp(target.source, target.flags);
        hash.set(target, res);
        return res;
    }

    /**
     * 函数默认浅拷贝一下即可,没有其他的处理,没办法哈哈
     */
    if (typeof target === "function") {
        return target;
    }

    /**
     * Map 的键key 和值 value 都可以是任意的数据类型,所以说他的处理情况需要考虑更多的东西吧
     */
    if (target instanceof Map) {
        res = new Map();
        hash.set(target, res);
        target.forEach((value, key) => {
            res.set(deepClone(key, hash), deepClone(value, key));
        })
        return res;
    }

    /**
     * set 的基本操作和数组是差不多的讷,但是需要额外注意的是我们的数组中的元素必须是:
     * 可能是引用的数据类型吧,这里需要额外的注意一下吧
     */
    if (target instanceof Set) {
        res = new Set();
        hash.set(target, res);
        target.forEach(val => {
            res.add(deepClone(val, hash))
        });
        return res;
    }

    /**
     * 这两个进行归一化的处理的核心原因是其键key只能是基本的数据类型吧
     * 1. 对于数据而言的话,key 只能是我们的数组 number 的数据类型吧
     * 2. 对于对象的话,键除了是字符串就是我们的Symbol数据类型吧
     * 3. 但是他们的value就说不定了讷!都可能是引用的数据类型吧
     */
    res = Array.isArray(target) ? [] : {};
    hash.set(target, res);

    Reflect.ownKeys(target).forEach(key => {
        res[key] = deepClone(target, hash);
    })

    return res;
}

深比较函数

  • 基本类型:直接用===比较;

  • 引用类型:

    • 先判断类型是否一致;

    • 再判断属性数量是否一致;

    • 最后递归比较每个属性的值

function isEqual(a, b) {
    // 基本数据类型直接比较即可吧
    if (a === b) {
        return true;
    }
    // 对于 NaN 情况额外需要处理一下吧
    if (Number.isNaN(a) && Number.isNaN(b)) {
        return true;
    }
    // null 和 undefined 的情况吧
    if (a === null
        || b === null
        || typeof a !== "object"
        || typeof b !== "object"
    ) {
        return false;
    }

    // 开始进行判断引用数据类型了吧
    if (a instanceof Date && b instanceof Date) {
        return a.getTime() === b.getTime();
    }

    if (a instanceof RegExp && b instanceof RegExp) {
        return a.source === b.source && a.flags === b.flags;
    }

    // 构造函数的情况吧
    if (a.constructor !== b.constructor) {
        return false;
    }

    if (Array.isArray(a) && Array.isArray(b)) {
        if (a.length !== b.length) {
            return false;
        }
        for (let i = 0; i < a.length; i++) {
            if (isEqual(a[i], b[i])) {
                return true
            }
            return false;
        }
    }

    if (!Array.isArray(a) && !Array.isArray(b)) {
        const keysA = Reflect.ownKeys(a);
        const keysB = Reflect.ownKeys(b);

        if (keysA.length !== keysB.length) {
            return false;
        }
        for (const key of keysA) {
            if (!isEqual(a[key], b[key])) {
                return false;
            }
            return true;
        }
    }

    return false;
}

Promise.all手写

  • Promise.all 接收的是一个 Promise 数组吧,返回的是一个新的 Promise 吧,所有的Promise 都是成功的时候,才会进行返回数组吧,顺序和原数组是一样的讷,只有一个失败的时候才会 reject 出具体的失败的 reason 吧

  • 原理是

    • 遍历 promise 数组,监听每个 promise 的状态吧

    • 使用计数器进行记录 promise 的数量,收集结果即可吧

    • 所有的成功后,resolve 结果即可吧

function isIterable(target) {
    return target !== null && typeof target[Symbol.iterator] === 'function'
}

function myPromiseAll(primises) {
    if (!isIterable(primises)) {
        console.log("错了错了错了")
        return;
    }

    const promiseArr = Array.from(promises);
    const len = promiseArr.length;
    const res = [];
    const resolveCount = 0;

    return new Promise((resolve, reject) => {
        if (len === 0) {
            resolve(res);
            return;
        }
        promiseArr.forEach((p, index) => {
            Promise.resolve(p).then(
                value => {
                    resolveCount++;
                    res[index] = value;
                    if (resolveCount === len) {
                        resolve(res)
                    }
                },
                reason => {
                    reject(reason);
                }
            )
        })
    })
}

function myPromiseRace(promises) {
  if (!isIterable(promises)) {
    throw new TypeError(promises + ' is not iterable');
  }
  const promiseArr = Array.from(promises);

  return new Promise((resolve, reject) => {
    // 遍历每个Promise,第一个完成的触发resolve/reject
    promiseArr.forEach(p => {
      Promise.resolve(p).then(
        (value) => resolve(value), // 第一个成功,resolve
        (reason) => reject(reason) // 第一个失败,reject
      );
    });
  });
}

数组分组和去重

  • 数组分组:按指定字段将数组对象分组(如按 type 分组);

  • 数组去重:去除数组中的重复元素(基础类型 / 对象类型)。

// 1. 数组分组(按指定字段)
function groupBy(arr, key) {
  return arr.reduce((acc, cur) => {
    const groupKey = cur[key];
    if (!acc[groupKey]) {
      acc[groupKey] = [];
    }
    acc[groupKey].push(cur);
    return acc;
  }, {});
}

function myGroupBy(arr, key) {
    const res = {};
    arr.forEach(item => {
        const groupKey = item[key];
        if (!res[groupKey]) {
            res[groupKey] = [];
        }
        res[groupKey].push(item);
    });
    return res;
}

// 测试分组
const data = [{ type: 'vue', id: 1 }, { type: 'react', id: 2 }, { type: 'vue', id: 3 }];
console.log(groupBy(data, 'type')); // { vue: [{...}, {...}], react: [{...}] }

// 2. 数组去重(基础类型)
function uniqueBasic(arr) {
  return [...new Set(arr)];
}

// 3. 数组对象去重(按指定字段)
function uniqueObject(arr, key) {
  const set = new Set();
  return arr.filter(item => {
    const id = item[key];
    if (set.has(id)) {
      return false;
    }
    set.add(id);
    return true;
  });
}

// 测试去重
const objArr = [{ id: 1, name: 'a' }, { id: 1, name: 'a' }, { id: 2, name: 'b' }];
console.log(uniqueObject(objArr, 'id')); // [{id:1,name:'a'}, {id:2,name:'b'}]

URL 解析和拼接

function urlParseParams(url) {
    // 1. 获取得到查询参数吧
    const paramsStr = url.includes("?") ? url.split("?") : "";
    const params = {};

    if (!paramsStr) {
        return params;
    }

    paramsStr.split("&").forEach(item => {
        const [key, value] = item.split("=");
        const decodeKey = decodeURIComponent(key || "");
        const decodeValue = decodeURIComponent(value || "");
        if (decodeKey) {
            params[decodeKey] = decodeValue;
        }
    })

    return params;
}

function urlStringifyParams(params) {
    if (typeof params !== "object" || params === null) {
        return ''
    }
    const arr = [];

    Object.entries(params).forEach(([key, value]) => {
        if (value === undefined || value === null) return;
        const encodeKey = encodeURIComponent(key);
        const encodeValue = encodeURIComponent(value);
        arr.push(`${encodeKey}=${encodeValue}`);
    });

    return arr.join('&');
}

函数式编程

函数柯里化

柯里化:将接收多个参数的函数,转化为接收单个参数的函数,返回接收剩余参数的新函数,直到参数全部传入才执行。

  • 收集参数,直到参数数量达到原函数的参数数量;

  • 未收集完则返回新函数,收集完则执行原函数。

function curry(fn) {
    const fnArgsLength = fn.length;
    const args = [];

    const curried = function(...newArgs) {
        args.push(...newArgs);
        if (args.length < fnArgsLength) {
            return curried
        }
        return fn.apply(this, args,slice(0, fnArgsLength));
    }

    return curried
}

组合函数/管道函数

  • compose:从右到左执行函数组合(compose(f,g,h) → x => f(g(h(x))));

  • pipe:从左到右执行函数组合(pipe(f,g,h) → x => h(g(f(x))))。

// compose:右→左
function compose(...fns) {
    if (fns.length === 0) return x => x;
    if (fns.length === 1) return fns[0];
    return fns.reduce((prevFn, curFn) => {
        (...args) => prevFn(curFn(...args));   
    })
}

// pipe:左→右
function pipe(...fns) {
    if (fns.length === 0) return (x) => x;
    if (fns.length === 1) return fns[0];
    return fns.reduce((a, b) => (...args) => b(a(...args)));
}

算法题

快速排序

function quickSort(arr) {
    if (arr.length <= 1) return arr;

    const pivotIndex = Math.floor(arr.length / 2);
    const pivot = arr.splice(pivotIndex, 1)[0];  // 获取得到基准值从原数组中进行删除吧

    const left = []
    const right = []

    for (const num of arr) {
        if (arr < pivot) {
            left.push(num)
        } else {
            right.push(num)
        }
    }

    return quickSort(left).concat([pivot], quickSort(right))
}

插入排序

function insertionSort(arr) {
  if (arr.length <= 1) return arr;

  const len = arr.length;
  // 从第二个元素开始,逐个插入有序区间
  for (let i = 1; i < len; i++) {
    const current = arr[i]; // 待插入元素
    let j = i - 1; // 有序区间的最后一个元素索引

    // 向前遍历有序区间,找到插入位置(比current大的元素后移)
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j];
      j--;
    }
    // 插入当前元素到正确位置
    arr[j + 1] = current;
  }
  return arr;
}

二分搜索

function binarySearch(arr, target) {
    if (arr.length === 0) {
        return -1;
    }

    let left = 0;
    let right = arr.length - 1;

    while (left < right) {
        const mid = left + Math.floor(right - left) / 2;

        if (arr[mid] === target) {
            return mid
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    return -1;
}

两数之和

function twoSum(nums, target) {
  if (!Array.isArray(nums) || nums.length < 2) return [];

  const map = new Map(); // 存储{数值: 索引}
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i]; // 补数:目标和 - 当前数
    if (map.has(complement)) {
      return [map.get(complement), i]; // 找到补数,返回索引
    }
    map.set(nums[i], i); // 未找到,存入当前数和索引
  }
  return []; // 无符合条件的数
}

合并两个有序数组

function merge(nums1, m, nums2, n) {
  // 从后往前合并,避免覆盖nums1的有效元素
  let i = m - 1; // nums1有效末尾
  let j = n - 1; // nums2末尾
  let k = m + n - 1; // nums1总末尾

  // 遍历nums2,将较大的数放入nums1末尾
  while (i >= 0 && j >= 0) {
    if (nums1[i] > nums2[j]) {
      nums1[k] = nums1[i];
      i--;
    } else {
      nums1[k] = nums2[j];
      j--;
    }
    k--;
  }

  // 若nums2还有剩余元素,直接复制到nums1前面
  while (j >= 0) {
    nums1[k] = nums2[j];
    j--;
    k--;
  }
}

反转字符串

function reverseStr(str) {
  if (typeof str !== 'string' || str.length <= 1) return str;

  // 字符串不可变,转数组处理
  const arr = str.split('');
  let left = 0;
  let right = arr.length - 1;
  while (left < right) {
    // 交换左右指针元素
    [arr[left], arr[right]] = [arr[right], arr[left]];
    left++;
    right--;
  }
  return arr.join('');
}

回文字符串判断

/**
 * 回文判断(忽略空格、大小写)
 * @param {string} str - 输入字符串
 * @returns {boolean} 是否为回文
 */
function isPalindrome(str) {
  if (typeof str !== 'string') return false;

  // 预处理:去除空格,转小写
  const processedStr = str.replace(/\s+/g, '').toLowerCase();
  let left = 0;
  let right = processedStr.length - 1;

  while (left < right) {
    if (processedStr[left] !== processedStr[right]) {
      return false;
    }
    left++;
    right--;
  }
  return true;
}

有效括号判断

function isValidBracket(s) {
  if (typeof s !== 'string') return false;
  // 边界:奇数长度直接返回false
  if (s.length % 2 !== 0) return false;

  const stack = [];
  // 括号映射:右括号→左括号
  const bracketMap = {
    ')': '(',
    ']': '[',
    '}': '{'
  };

  for (const char of s) {
    if (bracketMap[char]) {
      // 右括号:判断栈顶是否为对应左括号
      const top = stack.pop() || '';
      if (top !== bracketMap[char]) {
        return false;
      }
    } else {
      // 左括号:入栈
      stack.push(char);
    }
  }

  // 栈为空则所有括号匹配
  return stack.length === 0;
}

反转字符串

// 定义链表节点
function ListNode(val) {
  this.val = val;
  this.next = null;
}

/**
 * 反转单链表(双指针法)
 * @param {ListNode} head - 链表头节点
 * @returns {ListNode} 反转后的头节点
 */
function reverseLinkedList(head) {
  // 边界条件:空链表或单节点链表
  if (!head || !head.next) return head;

  let prev = null; // 前一个节点
  let cur = head; // 当前节点

  while (cur) {
    const next = cur.next; // 保存下一个节点(防断链)
    cur.next = prev; // 反转当前节点的指向
    // 指针后移
    prev = cur;
    cur = next;
  }

  // prev是最后一个节点,即反转后的头节点
  return prev;
}

链表环

/**
 * 判断链表是否有环(快慢指针)
 * @param {ListNode} head - 链表头节点
 * @returns {boolean} 是否有环
 */
function hasCycle(head) {
  if (!head || !head.next) return false;

  let slow = head; // 慢指针:每次走1步
  let fast = head.next; // 快指针:每次走2步

  while (slow !== fast) {
    // 快指针走到末尾,无环
    if (!fast || !fast.next) return false;
    slow = slow.next;
    fast = fast.next.next;
  }
  // 快慢指针相遇,有环
  return true;
}

dfs 和 bfs

/**
 * DFS-前序遍历(根 → 左 → 右)
 * @param {TreeNode} node - 当前节点
 * @param {number[]} res - 存储遍历结果
 */
function dfsPreOrder(node, res = []) {
  if (!node) return; // 边界:空节点直接返回
  res.push(node.val); // 1. 处理根节点
  dfsPreOrder(node.left, res); // 2. 递归左子树
  dfsPreOrder(node.right, res); // 3. 递归右子树
  return res;
}

/**
 * DFS-中序遍历(左 → 根 → 右)
 * @param {TreeNode} node - 当前节点
 * @param {number[]} res - 存储遍历结果
 */
function dfsInOrder(node, res = []) {
  if (!node) return;
  dfsInOrder(node.left, res); // 1. 递归左子树
  res.push(node.val); // 2. 处理根节点
  dfsInOrder(node.right, res); // 3. 递归右子树
  return res;
}

/**
 * DFS-后序遍历(左 → 右 → 根)
 * @param {TreeNode} node - 当前节点
 * @param {number[]} res - 存储遍历结果
 */
function dfsPostOrder(node, res = []) {
  if (!node) return;
  dfsPostOrder(node.left, res); // 1. 递归左子树
  dfsPostOrder(node.right, res); // 2. 递归右子树
  res.push(node.val); // 3. 处理根节点
  return res;
}



function dfsPreOrderIteration(root) {
  if (!root) return [];
  const stack = [root]; // 栈初始化,存入根节点
  const res = [];

  while (stack.length) {
    const node = stack.pop(); // 弹出栈顶节点(后进先出)
    res.push(node.val); // 处理当前节点
    // 先压右子树,再压左子树(栈后进先出,保证左子树先处理)
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }
  return res;
}
function bfsLevelOrder(root) {
  if (!root) return [];
  const queue = [root]; // 队列初始化,存入根节点
  const res = [];

  while (queue.length) {
    const levelSize = queue.length; // 当前层的节点数
    const levelRes = []; // 存储当前层的节点值(可选,按层分组用)

    // 遍历当前层所有节点
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift(); // 出队(先进先出)
      levelRes.push(node.val); // 处理当前节点
      // 入队下一层节点(左→右)
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    res.push(levelRes); // 按层分组存储
    // 若只需一维结果,直接 res.push(...levelRes) 即可
  }
  return res;
}