这10道前端常考的手写题,能轻松拿捏吗?

135 阅读6分钟

new


function myNew(constructor, ...args) {
  // 确保传入的 constructor 是一个函数
  if (typeof constructor !== 'function') {
    throw new TypeError('Constructor must be a function');
  }

  // 创建一个新对象,修改原型链
  const newObj = Object.create(constructor.prototype);

  // 使用 apply 调用构造函数,将参数传递给它
  const result = constructor.apply(newObj, args);

  // 如果构造函数返回的是对象或函数,则返回该对象或函数,否则返回新创建的对象
  return result && (typeof result === 'object' || typeof result === 'function')
    ? result
    : newObj;
}

防抖

/**
● 创建一个防抖函数 防抖的主要目的是减少函数调用的次数,只有在事件停止触发后的一段时间才执行相应的函数。 
● @param {Function} func - 要防抖的函数
● @param {number} wait - 防抖的时间间隔(以毫秒为单位)
● @returns {Function} - 返回一个防抖后的函数
 */
function debounce(func, wait) {
  let timeout; return function (...args) {
    const context = this;

    // 清除之前的定时器
    if (timeout) clearTimeout(timeout);

    // 设置一个新的定时器
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  };
}

// 使用示例
function performSearch(query) {
    console.log('Searching for:', query);
}

// 使用防抖函数处理搜索输入
const debouncedSearch = debounce(performSearch, 300);

// 模拟用户输入
debouncedSearch('apple');
debouncedSearch('banana');
debouncedSearch('cherry');

// 300毫秒后,最终会调用 performSearch('cherry')

节流

/**
● 创建一个节流函数 节流保证在特定的时间间隔内函数只会被调用一次,
● @param {Function} func - 要节流的函数
● @param {number} limit - 节流的时间间隔(以毫秒为单位)
● @returns {Function} - 返回一个节流后的函数
 */

function throttle(func, limit) {
  let lastFunc;
  let lastRan;

  return function (...args) {
    const context = this;
    const now = Date.now();
    if (!lastRan) {
      // 立即执行
      func.apply(context, args);
      lastRan = now;
    } else {
      // 延迟执行
      clearTimeout(lastFunc);
      lastFunc = setTimeout(() => {
        func.apply(context, args);
        lastRan = now;
      }, Math.max(limit - (now - lastRan), 0));
    }
  };
}
// 使用示例
const handleScroll = throttle(() => {
  console.log('Scroll event handler called');
}, 1000);
// 监听滚动事件
window.addEventListener('scroll', handleScroll);

冒泡

/**
 * 冒泡排序算法 它重复地遍历待排序的元素,通过相邻元素的比较和交换,把最大的元素“冒泡”到数组的末尾。这个过程会持续进行,直到整个数组都按升序排列。 复杂度为 𝑂(𝑛2)
 * @param {Array<number>} arr - 待排序的数组
 * @returns {Array<number>} - 排序后的数组
 */
function bubbleSort(arr) {
  let n = arr.length;
  let swapped;

  for (let i = 0; i < n - 1; i++) {
    swapped = false;

    for (let j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换相邻元素
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        swapped = true;
      }
    }

    // 如果在某一轮中没有发生交换,则说明数组已经排序好,提前退出循环
    if (!swapped) break;
  }

  return arr;
}

// 使用示例
const array = [64, 34, 25, 12, 22, 11, 90];
console.log('原始数组:', array);
const sortedArray = bubbleSort(array);
console.log('排序后的数组:', sortedArray);

选择排序


function selectionSort(arr) {
  let n = arr.length;
  
  // 遍历数组
  for (let i = 0; i < n - 1; i++) {
    // 假设当前元素是最小的
    let minIndex = i;
    
    // 查找剩余部分的最小元素
    for (let j = i + 1; j < n; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    
    // 如果找到了更小的元素,则交换
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  
  return arr;
}

// 示例使用
const array = [64, 25, 12, 22, 11];
console.log('Original array:', array);
console.log('Sorted array:', selectionSort(array));

实现promise.all()

function customPromiseAll(promises) {
    // 返回一个新的 Promise
    return new Promise((resolve, reject) => {
        // 存储每个 Promise 的结果
        const results = [];
        // 计数器,记录完成的 Promise 数量
        let completedCount = 0;

        // 检查传入的参数是否是一个可迭代的对象
        if (!Array.isArray(promises)) {
            return reject(new TypeError('Argument must be an array'));
        }

        // 如果传入的是空数组,直接返回一个 resolved 的 Promise
        if (promises.length === 0) {
            return resolve(results);
        }

        // 遍历每个 Promise
        promises.forEach((promise, index) => {
            // 确保每个项都是一个 Promise 对象
            Promise.resolve(promise)
                .then(value => {
                    // 将每个 Promise 的结果存储在结果数组中
                    results[index] = value;
                    completedCount++;

                    // 如果所有 Promise 都已完成,则 resolve 最终结果
                    if (completedCount === promises.length) {
                        resolve(results);
                    }
                })
                .catch(error => {
                    // 如果有任何一个 Promise 被拒绝,则 reject 整个 Promise
                    reject(error);
                });
        });
    });
}

// 示例使用
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = new Promise((resolve, reject) => setTimeout(() => resolve(3), 1000));

customPromiseAll([p1, p2, p3])
    .then(results => {
        console.log(results); // [1, 2, 3]
    })
    .catch(error => {
        console.error(error);
    });

树结构转换


function translateToTree(arr) {
  const map = new Map();
  const result = [];

  arr.forEach((item) => {
    map.set(item.id, { ...item });
  });

  arr.forEach((item) => {
    const node = map.get(item.id);
    const parent = map.get(item.pid);
    if (parent) {
      if (!parent.children) {
        parent.children = [];
      }
      parent.children.push(node);
    } else {
      result.push(node);
    }
  });

  return result;
}


// 示例数据
const source = [
  { id: 1, pid: 0, name: 'body' },
  { id: 2, pid: 1, name: 'title' },
  { id: 3, pid: 2, name: 'div' }
];

实现深复制

function deepClone(value, map = new WeakMap()) {
  // 如果值是 null 或不是对象,直接返回值
  if (value === null || typeof value !== 'object') {
    return value;
  }

  // 如果值是 Date 类型,返回一个新的 Date 对象
  if (value instanceof Date) {
    return new Date(value.getTime());
  }

  // 如果值是 RegExp 类型,返回一个新的 RegExp 对象
  if (value instanceof RegExp) {
    return new RegExp(value);
  }

  // 处理循环引用
  if (map.has(value)) {
    return map.get(value);
  }

  // 创建一个新对象或数组来存储深拷贝结果
  const copy = Array.isArray(value) ? [] : {};

  // 将当前对象存入 map
  map.set(value, copy);

  // 递归处理对象的每一个属性
  Object.keys(value).forEach(key => {
    copy[key] = deepClone(value[key], map);
  });

  return copy;
}

判断是否循环引用

function isCycleObject(obj) {
  const set = new WeakSet();

  function func(current) {
    // 如果当前值是 null 或非对象,直接返回 false
    if (current === null || typeof current !== "object") {
      return false;
    }

    // 如果当前对象已被访问过,说明存在循环引用
    if (set.has(current)) {
      return true;
    }

    // 将当前对象标记为已访问
    set.add(current);

    // 递归检查对象的每一个属性
    for (const key of Object.keys(current)) {
      if (func(current[key])) {
        return true;
      }
    }

    return false;
  }

  return func(obj);
}


// 示例
const objA = {};
const objB = { a: objA };
objA.b = objB;  // 循环引用

console.log(hasCircularReference(objA));  // 输出: true

const objC = { name: 'Alice' };
console.log(hasCircularReference(objC));  // 输出: false

转换千分位

function formatWithCommas(numberStr) {
  // 确保输入是有效的数字字符串
  if (isNaN(numberStr) || numberStr.trim() === '') {
    throw new Error("输入的字符串不是有效的数字");
  }

  // 将输入字符串分为整数部分和小数部分
  const [integerPart, decimalPart] = numberStr.split('.');

  // 处理整数部分,逆序遍历并逐步添加千分位分隔符
  let result = '';
  let count = 0;

  // 从右向左遍历整数部分
  for (let i = integerPart.length - 1; i >= 0; i--) {
    result = integerPart[i] + result;
    count++;
    if (count % 3 === 0 && i > 0) {
      result = ',' + result;
    }
  }

  // 重新拼接整数部分和小数部分
  return decimalPart ? `${result}.${decimalPart}` : result;
}

// 示例用法
try {
  const formattedStr = formatWithCommas("1234567890.12");
  console.log(formattedStr);  // 输出: 1,234,567,890.12
} catch (error) {
  console.error(error.message);
}

统计数组中元素出现次数

function countOccurrences(array) {
  const counts = {};
  
  for (const item of array) {
    counts[item] = (counts[item] || 0) + 1;
  }
  
  return counts;
}

// 示例
const array = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
console.log(countOccurrences(array));
// 输出: { apple: 3, banana: 2, orange: 1 }

实现flat

function newFlat(arr, n) {
  let result = [];

  arr.forEach((item) => {
    if (Array.isArray(item) && n > 0) {
      result = result.concat(newFlat(item), n - 1);
    } else {
      result.push(item);
    }
  });

  return result;
}

实现字符串中不重复的最大长度

var lengthOfLongestSubstring = function (s) {
  let map = new Map(); // 1. 创建一个 Map 用于记录每个字符的最新索引位置。
  let i = -1;          // 2. 初始化左指针 i 为 -1,用于表示当前窗口的左边界。
  let res = 0;         // 3. 初始化 res 变量来记录当前找到的最长不重复子串的长度。
  let n = s.length;   // 4. 获取字符串的长度并存储在变量 n 中。

  for (let j = 0; j < n; j++) { // 5. 遍历字符串中的每个字符,使用 j 作为右指针。
    if (map.has(s[j])) { // 6. 如果字符 s[j] 已经在 map 中出现过:
      i = Math.max(i, map.get(s[j])); 
      // 7. 更新左指针 i 为 map 中记录的字符 s[j] 的最新索引位置。
      // 这里使用 Math.max 是因为 i 应该是从字符出现的最新位置开始计算,以确保不重复。
    }
    res = Math.max(res, j - i); // 8. 更新 res 为当前窗口的长度 j - i。
    map.set(s[j], j); // 9. 更新 map 中字符 s[j] 的最新索引位置为 j。
  }

  return res; // 10. 返回找到的最长不重复子串的长度。
};