前端面试刷题必备(手撕代码篇)

4,980 阅读9分钟

前端面试刷题必备(手撕代码篇)

前端面试刷题(JS篇):juejin.cn/post/735241…

前端面试刷题必备(CSS篇):juejin.cn/post/735769…

前端面试刷题必备(性能优化篇):juejin.cn/post/735790…

前端面试刷题必备(React面试基础篇):juejin.cn/post/736070…

React原理相关(原理问题篇):juejin.cn/post/736257…

Call\Apply\Bind

Call

首先进行分析,call 的调用方式是 fn.call(thisArg, arg1, arg2, ...),它会立即调用一次,所以我们需要做的事情就是:

  1. 获取需要执行的函数(就是 this,因为 call 的调用都是 fn.call(),fn 是一个函数)
  2. 对 thisArg 进行转换成对象类型(防止传入的是一个非对象类型),这个 thisArg 就是需要绑定的 this
  3. 调用需要被执行的函数(通过给 thisArg 增加方法属性)
Function.prototype.myCall = function (thisArg, ...args) {
  let fn = this;
  thisArg =
    thisArg !== null && thisArg !== undefined ? Object(thisArg) : window;
  thisArg.fn = fn;
  const result = thisArg.fn(...args);
  delete thisArg.fn;
  return result;
};

Apply

首先进行分析,apply 的调用方式是 fn.apply(thisArg, [arg1, arg2, ...]),它会立即调用一次,所以我们需要做的事情就是:

  1. 获取需要执行的函数(就是 this,因为 apply 的调用都是 fn.apply(),fn 是一个函数)
  2. 对 thisArg 进行转换成对象类型(防止传入的是一个非对象类型),这个 thisArg 就是需要绑定的 this
  3. 需要对第二个参数进行一下判断,因为需要传入的是一个数组
  4. 调用需要被执行的函数(通过给 thisArg 增加方法属性)
Object.myApply = function (thisArg, args) {
  const fn = this;
  thisArg =
    thisArg === null || thisArg === undefined ? window : Object(thisArg);
  thisArg.fn = fn;
  args = args || [];
  const result = thisArg.fn(...args);
  delete thisArg.fn;
  return result;
};

Bind

bind 调用的时候不会立即执行,而是返回一个函数,所以我们需要做的事情就是:

  1. 获取需要执行的函数(就是 this,因为 bind 的调用都是 fn.bind(),fn 是一个函数)
  2. 对 thisArg 进行转换成对象类型(防止传入的是一个非对象类型),这个 thisArg 就是需要绑定的 this
  3. 返回一个函数
  4. 返回的函数内部调用需要被执行的函数(通过给 thisArg 增加方法属性)
  5. 调用时将参数传递进去,返回的函数可能能够接收到参数,所以需要将返回函数的参数也传入进去
Object.myBind = function (thisArg, ...args) {
  const fn = this;
  thisArg =
    thisArg === null || thisArg === undefined ? window : Object(thisArg);
  return function (...newArgs) {
    thisArg.fn = fn;
    const result = thisArg.fn(...args, ...newArgs);
    delete thisArg.fn;
    return result;
  };
};

传入普通函数转换成柯里化的函数

思路

  1. fn.length 可以获取到函数的参数个数,因此我们可以通过判断传入的参数是否大于等于需要的参数,如果 true 则直接执行
  2. 如果传入的参数小于需要的参数,则返回一个新的函数,继续接受参数
  3. 接受参数后,需要递归调用检查函数的个数是否达到
function hyCurrying(fn) {
  function curried(...arg) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      function curried2(...arg2) {
        return curried.apply(this, args.concat(arg2));
      }
      return curried2;
    }
  }
}

防抖函数

防抖函数,简单来说就是我们玩王者荣耀,当我们点击回城,在回城的过程中,总是按照最后一次点击的时间来回去。

常规写法

  1. 定义一个函数,这个函数会传入一个会被执行的方法,和防抖的时长
  2. 返回一个函数,这个函数就是处理好的防抖函数,可以传入被执行的函数所需要的参数
  3. 这边可以通过闭包的技术,在外部先定义一个 timer,然后执行防抖函数时先判断 timer 函数存在不存在,存在就清除定时器。
  4. 然后创建一个定时器,并赋值给 timer 变量,这是为了避免 this 指向被改变,所以我们需要使用 apply 来制定 this
function debounce(fn, delay) {
  let timer = null;
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
  return _debounce;
}

立即执行一次防抖

在上一个常规写法的基础上,我们增加了一个传参:immediate,表示是否开启立即执行;

然后和 timer 变量一样,利用闭包的技术,定义一个isInvoke,如果为 false 就是还没有立即执行,就去执行一次,并将isInvoke值改掉

function debounce2(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false;
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer);
    // 判断是否需要立即执行
    if (immediate && !isInvoke) {
      fn.apply(this, args);
      isInvoke = true;
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        fn.apply(this, args);
        isInvoke = false;
      }, delay);
    }
  };
  return _debounce;
}

带取消功能

我们在立即执行一次的基础上,再进行封装,让他支持回调操作,以及取消定时器。

取消定时器很简单,就是给返回的那边变量添加一个属性cancel,这个 cancel 用来清除定时器,以及通过闭包技术将 timer 和 isInvoke 设置为初始值;

支持回调则只需要在函数执行完毕后,判断函数是否有返回值,如果有,则传入回调函数中执行。

function debounce3(fn, delay, immediate = false, resultCallback) {
  let timer = null;
  let isInvoke = false;
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer);
    // 判断是否需要立即执行
    if (immediate && !isInvoke) {
      const result = fn.apply(this, args);
      if (resultCallback) resultCallback(result);
      isInvoke = true;
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        const result = fn.apply(this, args);
        if (resultCallback) resultCallback(result);
        isInvoke = false;
      }, delay);
    }
  };
  // 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer);
    timer = null;
    isInvoke = false;
  };
  return _debounce;
}

节流函数

理解:节流就像是我们玩王者荣耀的时候,点击一次技能后,技能释放,然后需要一定的 cd 时间,才能触发下一次的技能释放。

基本写法

  1. 定义一个函数,这个函数会传入需要执行的函数,以及执行的间隔时长。
  2. 函数内部定义一个最后执行的时间,初始化为 0;定义一个_throttle 函数,并返回这个函数,这个函数就是事件触发时需要执行的函数,比如点击事件。
  3. 同样通过闭包的技术,获取到这个_throttle 函数执行的时间,如果执行的间隔小于当次执行时间(nowTime) 和上一次执行时间(lastTime)的间隔,那么就执行传入的函数,并更改上一次执行时间(lastTime)。
function throttle(fn, interval) {
  let lastTime = 0;
  const _throttle = function () {
    const nowTime = new Date().getTime();
    const remainTime = interval - (nowTime - lastTime);
    if (remainTime <= 0) {
      fn.apply(this, args);
      lastTime = nowTime;
    }
  };
  return _throttle;
}

深拷贝

JSON 方法

这是通过将对象转换成字符串,然后再转换成对象实现的深拷贝,但是会存在一定的问题,比如:函数

const cloneObj = JSON.parse(JSON.stringify(obj));

递归方式实现

  1. 首先需要判断传入的对象是否是一个对象类型,如果不是对象类型直接返回即可。
  2. 如果是对象类型,那么就需要去递归调用 deepCopy 函数。
  3. 如果他的 key 是一个 Symbol, 就需要 Object.getOwnPropertySymbols 获取到 symbol 的 key,然后去递归调用。
  4. 然后对传入的数据进行一系列的判断,进行对应的处理。
  5. 如果是循环引用,就需要用到他的第二个参数 map,初始化是一个 WeakMap 数据,我们每次遍历的时候都会在 map 中将当前这个对象作为 WeakMap 的 key 值存起来,如果发现有一样的存在,那就说明存在递归调用,直接 return 对应的值。
function isObject(value) {
  const valueType = typeof value;
  return value !== null && (valueType === "object" || valueType === "function");
}

function deepCopy(originValue, map = new WeakMap()) {
  // 判断是否是一个Set类型,此处是浅拷贝
  if (originValue instanceof Set) {
    return new Set([...originValue]);
  }

  // 判断是否是一个Map类型
  if (originValue instanceof Map) {
    return new Map([...originValue]);
  }

  // 如果是Symbol类型
  if (typeof originValue === "symbol") {
    return Symbol(originValue.description);
  }

  // 如果是函数类型,那么直接使用同一个函数
  if (typeof originValue === "function") {
    return originValue;
  }

  // 判断传入的OriginValue是否是一个对象类型
  if (!isObject(originValue)) {
    return originValue;
  }

  // 处理循环引用的问题
  if (map.has(originValue)) {
    return map.get(originValue);
  }

  // 判断传入的对象是数组还是对象
  const newObject = Array.isArray(originValue) ? [] : {};

  // 处理循环引用的问题
  map.set(originValue, newObject);

  for (const key in originValue) {
    newObject[key] = deepCopy(originValue[key], map);
  }

  // 对symbol的key进行特殊处理
  const symbolKeys = Object.getOwnPropertySymbols(originValue);
  for (const sKeys of symbolKeys) {
    newObject[sKeys] = deepCopy(originValue[sKeys], map);
  }

  return newObject;
}

手写 Promise

具体可参考:juejin.cn/post/711237…

异步控制并发数

  1. 首先我们定义一个函数,这个函数会有 2 个参数,分别是请求的数组,以及最大并发数
  2. 这个函数会返回一个 Promise
  3. 我们会定义一个 len,len 就是请求 url 数组的长度;还有 1 个 count 变量,初始值为 0;如果后续 len 的长度和 count 的值一样就表示请求完了
  4. 然后我们需要定义一个 start 函数,用来去执行请求。
  5. 这个函数会从数组中拿出第一个 url 去请求,然后无论成功失败,都会进行一次判断 count 是不是和长度-1 相同,因为 count 是从 0 开始加的,如果相同就表示数组中的请求都请求完了,然后 promise 变成 fullfiled 的状态,否则 count 自增,继续执行 start 去取 url 请求。
  6. 这时我们需要使用 while 启动 limit 数量的任务,每个任务内部会自调用 start 函数
function limitRequest(urls = [], limit = 3) {
  return new Promise((resolve, reject) => {
    const len = urls.length;
    let count = 0;

    // 同时启动limit个任务
    while (limit > 0) {
      start();
      limit -= 1;
    }

    function start() {
      const url = urls.shift(); // 从数组中拿取第一个任务
      if (url) {
        axios
          .post(url)
          .then((res) => {
            // todo
          })
          .catch((err) => {
            // todo
          })
          .finally(() => {
            if (count == len - 1) {
              // 最后一个任务完成
              resolve();
            } else {
              // 完成之后,启动下一个任务
              count++;
              start();
            }
          });
      }
    }
  });
}

Promise.all 的实现

  1. Promise.all 实现起来相对比较简单的,首先他会传入一个 Promise 数组,然后返回一个 Promise
  2. 在返回的 Promise 传入的函数内部定义一个 result 用来接受数组的结果
  3. 遍历 promiseList 数组,然后如果是 rejected 直接抛出异常
  4. 如果是 fullfiled 的,则往 result 数组里添加执行结果,并判断 result 的长度是否和 promiseList 数组的长度相同,如果相同就表示执行完毕了,直接 resolve 一个结果。
  5. 补充:可以对 promiseList 数组的每一项进行校验,是否为 Promise
Promise.mpAll = function (promiseList = []) {
  return new Promise((resolve, reject) => {
    const result = []; // 结果数组
    promiseList.forEach((p) => {
      p.then((res) => {
        result.push(res);
        if (result.length === promiseList.length) {
          resolve(result);
        }
      }).catch((e) => {
        reject(e);
      });
    });
  });
};

冒泡排序

function bubbleSort(arr) {
  let len = arr.length;
  for (let i = 0; i < len - 1; i++) {
    // 从第一个元素开始,比较相邻的两个元素,前者大就交换位置
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        let num = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = num;
      }
    }
    // 每次遍历结束,都能找到一个最大值,放在数组最后
  }
  return arr;
}

实现 instanceof 方法

instanceof 会判断对象的原型及他的原型链上的原型是否是某个构造函数的原型。

  1. 定义一个函数,传如 2 个参数,分别为被判断的对象以及构造函数。
  2. 函数内部我们要获取到被判断的对象的原型proto以及构造函数的原型prototype
  3. 这时候需要开始循环,如果proto === prototype就说明对象的原型在这个构造函数的原型链上,返回 true
  4. 否则就获取到 proto 的原型继续循环
  5. 直到 proto 为 undefined
function myInstanceof(obj, ctor) {
  let proto = Object.getPrototypeOf(obj);
  let prototype = ctor.prototype;
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}