前端面试--JS手写代码题(一)

80 阅读7分钟

背景

去年年底12月份第一次被裁。今年5月份找到新的工作,最近又被裁了,还卡着我还有几天就满的六个月试用期。说我试用期没满,不给赔偿。嗐,一言难尽~

又得重新找工作了,所以搜集了一波比较高频的js手写代码题,记录一下。

下面所有代码都已经上传github仓库:github.com/ryan6015/fr…

并都带有测试用例。

  1. 模拟new操作符

首先要知道new操作符有哪些操作:

  1. 创建一个空对象
  2. 把这个对象的原型指向函数的prototype对象
  3. 将函数作用于指向这个对象并执行构造函数,
  4. 返回对象
/**
 * 实现一个函数模拟new操作符
 *
 * 首先想清楚在new的过程中,有哪些操作
 * 1. 创建一个空对象
 * 2. 对象的原型指向函数的prototype对象
 * 3. 执行构造函数,
 * 4. 返回对象(构造函数一般不会返回东西,但是也有可能,如果返回的是对象的话,那么就返回这个对象)
 * 
 * @param {*} fn 构造函数
 * @param  {...any} args 参数
 */
function _new(fn, ...args) {
  if (typeof fn !== "function") {
    throw new TypeError("fn must be a function");
  }
  const obj = {};
  // 对象的原型指向函数的prototype对象
  Object.setPrototypeOf(obj, fn.prototype)
  // 执行构造函数,将this指向obj
  const res = fn.apply(obj, args);
  // 如果构造函数返回的是一个对象,那么就返回这个对象,否则返回obj
  return typeof res === "object" ? res : obj;
}
  1. 模拟call函数实现

call函数会改变函数中this的指向,并且同时接收多个参数,执行函数。

我们在模拟实现中要改变this的指向,可以把函数放在那个对象上运行。

/**
 * 实现一个函数,模拟call方法实现
 *
 * call函数改变了this的指向,并且执行了函数
 * 想要改变函数中this的指向,我们可以把函数放到某个对象中,然后在对象中执行函数
 * 
 * @param {funtion} fn 执行函数
 * @param {object} context 执行函数的上下文
 * @param  {...any} args 参数
 */
function _call(fn, context, ...args) {
  if (typeof fn !== "function") {
    throw new TypeError("fn must be a function");
  }
  // 如果context为null,那么就指向window
  // 这里jest运行在node环境上,所以没有window,用global代替
  // 如果context是一个基本类型,那么就把它转换成对象类型
  const ctx = context ? Object(context) : global;
  // 把函数放到对象中, 这里会有重名的风险,简单写下,重要的是是原理
  // 所以忽略这个问题,在实际开发中,不要这样做
  ctx._fn = fn;
  // 执行函数
  const result = ctx._fn(...args);
  // 删除函数
  delete ctx._fn;
  // 返回结果
  return result;
}
  1. 模拟apply函数实现

apply和call的差别就是接收参数形式的不同。

apply函数接收一个数组,call接收多个参数。

/**
 * 实现一个函数,模拟apply方法实现
 *
 * apply和call的区别就在于,接受的参数apply接受的是一个数组,call接受的是多个参数
 *
 * @param {funtion} fn 执行函数
 * @param {object} context 执行函数的上下文
 * @param  {any[]} args 参数
 */
function _apply(fn, context, args) {
  if (typeof fn !== "function") {
    throw new TypeError("fn must be a function");
  }
  // 如果context为null,那么就指向window
  // 这里jest运行在node环境上,所以没有window,用global代替
  // 如果context是一个基本类型,那么就把它转换成对象类型
  const ctx = context ? Object(context) : global;
  // 把函数放到对象中, 这里会有重名的风险,简单写下,重要的是是原理
  // 所以忽略这个问题,在实际开发中,不要这样做
  ctx._fn = fn;
  // 执行函数
  const result = ctx._fn(...args);
  // 删除函数
  delete ctx._fn;
  // 返回结果
  return result;
}
  1. 模拟bind函数实现

bind函数相对于call和apply会复杂一些。先看看bind函数原本的功能: MDN-bind

  1. 返回一个新的函数,绑定函数的this指向,
  2. 同时接收不定量的参数,这些参数会在函数运行前放在前面传给函数
  3. 要注意的是:直接new的方式调用bind函数,那么this指向的是新创建的对象,而不是context
  4. 如果context为空,指向全局变量
/**
 * 模拟bind函数实现
 *
 * 首先想想bind函数内部做了啥,
 * 1. 返回一个新的函数,绑定函数的this指向
 * 2. 同时接收不定量的参数,这些参数会在函数运行前放在前面传给函数
 * 3. 要注意的是:直接new的方式调用bind函数,那么this指向的是新创建的对象,
 *    而不是context
 * 4. 如果context为空,指向全局变量
 *
 * @param {function} fn 执行函数
 * @param {object} context 执行函数的上下文
 * @param  {...any} args 参数
 */
function _bind(fn, context, ...args) {
  if (typeof fn !== "function") {
    throw new TypeError("fn must be a function");
  }

  const boundFunction = function (...argLists) {
    let ctx = context ? Object(context) : global;
    // 这里判断this是否是func的实例(是否使用new),如果是,那么就返回this,
    if (this instanceof boundFunction) {
      return new fn(args.concat(argLists));
    } else {
      // 要加上之前bind时传入的参数,有点像是函数柯里化
      return fn.apply(ctx, args.concat(argLists));
    }
  };

  // 将函数的原型指向fn的原型,这样在使用new时,也可以继承fn的原型
  boundFunction.prototype = Object.create(fn.prototype);
  boundFunction.prototype.constructor = boundFunction;

  return boundFunction;
}
  1. 实现一个深度克隆函数

深度克隆的话,平时比较简单的情况可能都会使用JSON.parse(JSON.stringify(obj)) ,这个可以实现深拷贝,但是存在诸多限制。

  1. 对一些特殊类型的值无法处理(可以查看下面控制台打印截图)

    1. 会忽略函数类型,
    2. 正则对象会变成空对象,
    3. 日期对象会变成字符串,
    4. 会忽略symbol类型,
    5. set,map类型会变成空对象
  2. 循环引用会报错。

下面用另种一种方式兼容更多的类型

function deepClone(obj) {
  // 处理特殊类型, undefined的typeof是undefined
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  let res;
  if (Array.isArray(obj)) {
    res = [];
    for (let i = 0; i < obj.length; i++) {
      res[i] = deepClone(obj[i]);
    }
  } else {
    res = {};
    Object.keys(obj).forEach((key) => {
      res[key] = deepClone(obj[key]);
    });
  }

  return res;
}

上面这种方式,可以兼容更多的类型,还是没法处理循环引用的情况。如果要解决循环引用的问题,可以这么写:

function deepCloneWithMap(obj, map = new WeakMap()) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  if (map.has(obj)) {
    return map.get(obj);
  }

  let res;
  if (Array.isArray(obj)) {
    res = [];
  } else {
    res = {};
  }
  // map的设置必须在循环之前,否则会无限递归
  map.set(obj, res);

  if (Array.isArray(obj)) {
    for (let i = 0; i < obj.length; i++) {
      res[i] = deepCloneWithMap(obj[i], map);
    }
  } else {
    Object.keys(obj).forEach((key) => {
      res[key] = deepCloneWithMap(obj[key], map);
    });
  }

  return res;
}

用map保存已经拷贝过得对象,如果对象再次拷贝的话,那么就直接取出来,避免无限循环。

循环引用的这个贴一个测试用例:

test("测试循环引用 deepCloneWithMap", () => {
    const originalObj = {
      name: "John",
      age: 30,
      hobbies: [
        "reading",
        {
          book: "abc",
          self: this,
        },
      ],
      address: {
        city: "New York",
        street: "Main St",
      },
    };

    // 制造循环引用
    originalObj.self = originalObj;

    const clonedOriginalObj = deepCloneWithMap(originalObj);
    expect(clonedOriginalObj).toEqual(originalObj);
  });
  1. 实现防抖函数

防抖的核心思想是在事件被频繁触发的过程中,只让函数在最后一次触发事件后的一段特定延迟时间之后执行,如果在延迟时间内事件又被触发了,那么就重新计时延迟时间,直到延迟时间内没有新的触发,函数才会执行。简单来说,就是多次触发同一个事件,只执行最后一次触发对应的操作。

/**
 * 防抖
 * @param {*} fn 函数
 * @param {*} delay 延迟时间
 */
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    if (timer) {
      clearTimeout(timer);
    }
    const context = this;
    timer = setTimeout(() => {
      timer = null;
      fn.apply(context, args);
    }, delay);
  };
}
  1. 实现节流函数

节流的目的是限制函数在一定时间内只能被触发一次,无论这段时间内事件被触发了多少次,函数都只会执行一次,然后在规定的时间间隔过去之后,才可以再次执行。它就像是水龙头限流一样,按照固定的时间间隔来 “放水”(执行函数)。

/**
 * 节流
 * @param {*} fn 函数
 * @param {*} delay 延迟时间
 */
function throttle(fn, delay) {
  let timer = null;
  // 这里设置成0,首次调用会立即执行
  // 如果希望首次调用也要满足延时时间,就设置成Date.now()
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();
    const remaining = delay - (now - lastTime);
    const context = this;
    // 空闲,但是还没到时间,重新设置定时器
    if (!timer && remaining > 0) {
      timer = setTimeout(() => {
        fn.apply(context, args);
        timer = null;
        lastTime = now;
      }, remaining);
    } else if (!timer && remaining <= 0) {
      // 空闲,并且时间到了
      fn.apply(context, args);
      lastTime = now;
    }
  };
}

上面所有代码都已经上传github仓库:github.com/ryan6015/fr…

上面代码如果存在问题的话,欢迎在评论区指出。

下一篇文章: Promise静态方法实现--JS手写代码题(二)