前端面试中常见的手写代码

117 阅读5分钟

手写代码系列

将数字以逗号进行千分位分割

let num = 1232456789.98765;
let formattedNum = num.toLocaleString('en-US');
console.log(formattedNum); // 1,232,456,789.988

上述代码如何输出保留两位小数的字符串?

let num = 1232456789.98765;
let formattedNum = Number(num.toFixed(2)).toLocaleString('en-US');
console.log(formattedNum); // 1,232,456,789.99

防抖与节流

防抖

function debounce(fn, wait) {
  let timer;
  return function (...params) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, params), wait);
  };
}

节流

function throttle(fn, limit) {
  let isThrottle;
  return function (...params) {
    if (!isThrottle) {
      fn.apply(this, params);
      isThrottle = true;
      setTimeout(() => (isThrottle = false), limit);
    }
  };
}

浅拷贝与深拷贝

浅拷贝

浅拷贝是指创建一个新对象,并将原始对象的属性直接复制到新对象中。如果原始对象的属性是基本类型(如字符串、数字、布尔值等),那么这些值会被直接复制;但如果属性是引用类型(如对象、数组等),那么这些引用会被复制,也就是说新对象的属性会指向原始对象的属性所指向的同一块内存。

function shallowCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // 如果不是对象,则直接返回
  }

  let copy;
  if (Array.isArray(obj)) {
    copy = []; // 创建一个新数组
  } else {
    copy = {}; // 创建一个新对象
  }

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = obj[key]; // 直接复制属性
    }
  }

  return copy;
}

// 测试
const original = {
  name: 'Alice',
  age: 25,
  address: {
    city: 'New York',
    zip: 10001,
  },
  hobbies: ['reading', 'traveling'],
};

const shallowCopied = shallowCopy(original);

console.log(shallowCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: [ 'reading', 'traveling' ] }

shallowCopied.address.city = 'San Francisco';
shallowCopied.hobbies.push('swimming');

console.log(original); // 输出:{ name: 'Alice', age: 25, address: { city: 'San Francisco', zip: 10001 }, hobbies: [ 'reading', 'traveling', 'swimming' ] }
console.log(shallowCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'San Francisco', zip: 10001 }, hobbies: [ 'reading', 'traveling', 'swimming' ] }

深拷贝

深拷贝是指创建一个新对象,并递归地复制原始对象及其所有属性。无论是基本类型还是引用类型,都会创建一个新的实例,从而确保新对象与原始对象没有任何共享的引用。

在实现深拷贝时,需要考虑以下边界情况:

  1. 循环引用
    • 如果原始对象中存在循环引用,递归拷贝会导致无限递归。
    • 解决方案是使用一个 Map 来跟踪已经拷贝过的对象,避免重复拷贝相同的对象。
  2. 特殊类型的处理
    • 需要考虑特殊类型的对象,如日期、正则表达式、函数等。
    • 对于这些类型,可以直接拷贝它们的值或创建新的实例。
  3. 原型链
    • 深拷贝通常只复制对象自身的属性,不会复制原型链上的属性。
    • 如果需要复制原型链上的属性,需要额外处理。
  4. 错误处理
    • 需要考虑非对象类型的处理,例如 nullundefined 等。
function deepCopy(obj, seen = new WeakMap()) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // 如果不是对象,则直接返回
  }

  if (seen.has(obj)) {
    return seen.get(obj); // 如果已经拷贝过,则直接返回已拷贝的对象
  }

  let copy;
  if (Array.isArray(obj)) {
    copy = []; // 创建一个新数组
  } else if (obj instanceof Date) {
    copy = new Date(obj); // 处理日期对象
  } else if (obj instanceof RegExp) {
    copy = new RegExp(obj); // 处理正则表达式对象
  } else {
    copy = {}; // 创建一个新对象
  }

  seen.set(obj, copy); // 标记已经拷贝过的对象

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopy(obj[key], seen); // 递归深拷贝属性
    }
  }

  return copy;
}

// 测试
const original = {
  name: 'Alice',
  age: 25,
  address: {
    city: 'New York',
    zip: 10001,
  },
  hobbies: ['reading', 'traveling'],
  nested: {
    more: {
      info: 'deeply nested object',
    },
  },
  date: new Date(),
  regex: /test/,
  loop: null,
};

// 创建循环引用
original.loop = original;

const deepCopied = deepCopy(original);

console.log(deepCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: [ 'reading', 'traveling' ], nested: { more: { info: 'deeply nested object' } }, date: [Date], regex: [RegExp], loop: [Circular] }

deepCopied.address.city = 'San Francisco';
deepCopied.hobbies.push('swimming');
deepCopied.nested.more.info = 'changed';
deepCopied.date.setDate(deepCopied.date.getDate() + 1);
deepCopied.regex.lastIndex = 1;

console.log(original); // 输出:{ name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: [ 'reading', 'traveling' ], nested: { more: { info: 'deeply nested object' } }, date: [Date], regex: [RegExp], loop: [Circular] }
console.log(deepCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'San Francisco', zip: 10001 }, hobbies: [ 'reading', 'traveling', 'swimming' ], nested: { more: { info: 'changed' } }, date: [Date], regex: [RegExp], loop: [Circular] }

模拟实现一个new操作符

function _new(constructor, ...args) {
  // Step 1: 创建一个新的空对象
  const newObj = Object.create(constructor.prototype);

  // Step 2: 调用构造函数并传入参数
  const result = constructor.apply(newObj, args);

  // Step 3: 判断返回值,如果是对象或函数,则返回,否则返回新创建的对象
  return (typeof result === 'object' && result !== null) || typeof result === 'function' ? result : newObj;
}

// 示例构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}, and I am ${this.age} years old.`);
};

// 使用手写的 new 操作符
const person = _new(Person, 'Bob', 25);

person.greet(); // 输出: Hello, my name is Bob, and I am 25 years old.

函数柯里化

函数柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。下面是一个简单的函数柯里化的实现示例:

function curry(fn) {
  const arity = fn.length; // 获取原函数需要的参数数量

  return function curried(...args) {
    // 如果提供的参数数量不足,则返回一个新的函数
    if (args.length < arity) {
      return function(...moreArgs) {
        return curried(...args, ...moreArgs);
      };
    }
    // 参数数量足够时,调用原函数
    return fn(...args);
  };
}

// 示例使用
function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);

// 调用
console.log(curriedSum(1)(2)(3)); // 输出: 6
console.log(curriedSum(1)(2, 3)); // 输出: 6
console.log(curriedSum(1, 2)(3)); // 输出: 6
console.log(curriedSum(1, 2, 3)); // 输出: 6

实现数组任意层级的flat函数

可以使用ES10的flat函数,第二个参数传入Infinity

let arr= [1, [2, [3, [4]], 5]];  
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5]

还可以使用递归的办法:

function deepFlat(arr) {
  // 检查是否已经是扁平数组
  if (!Array.isArray(arr) || !arr.some(item => Array.isArray(item))) {
    return arr.slice(); // 如果不是数组或已经是扁平数组,直接返回
  }

  return arr.reduce((acc, val) => {
    // 如果当前项是数组,则递归调用 deepFlat 函数
    return acc.concat(Array.isArray(val) ? deepFlat(val) : val);
  }, []);
}

// 示例
const nestedArray = [1, 2, [3, 4, [5, 6]], 7, [8, [9, [10]]]];
const flatArray = deepFlat(nestedArray);
console.log(flatArray); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

使用Promise来封装AJAX请求

function ajax(url, options = {}) {
  return new Promise((resolve, reject) => {
    // 创建 XMLHttpRequest 对象
    const xhr = new XMLHttpRequest();

    // 设置默认选项
    const defaultOptions = {
      method: 'GET', // 默认使用 GET 方法
      headers: {
        'Content-Type': 'application/json',
      },
    };

    // 合并默认选项和传入的选项
    const finalOptions = { ...defaultOptions, ...options };

    // 初始化请求
    xhr.open(finalOptions.method, url, true);

    // 设置请求头
    Object.keys(finalOptions.headers).forEach(key => {
      xhr.setRequestHeader(key, finalOptions.headers[key]);
    });

    // 发送请求
    xhr.send(finalOptions.body);

    // 监听请求状态变化
    xhr.onreadystatechange = function () {
      if (xhr.readyState === XMLHttpRequest.DONE) { // 请求已完成
        if (xhr.status >= 200 && xhr.status < 300) { // 成功响应
          try {
            const data = JSON.parse(xhr.responseText);
            resolve(data);
          } catch (e) {
            reject(e);
          }
        } else { // 失败响应
          reject(new Error(`Request failed with status code ${xhr.status}`));
        }
      }
    };

    // 错误处理
    xhr.onerror = function () {
      reject(new Error('Network error'));
    };
  });
}

// 示例使用
ajax('/api/data')
  .then(data => console.log('Success:', data))
  .catch(error => console.error('Error:', error));