JavaScript 常见手写题汇总

4,409 阅读22分钟

前言

之前有陆续写过一些 JavaScript 手写题的文章,但是时间有点久了而且感觉写的太散了,于是我把之前的文章都删了,用这一篇文章做一个汇总,面试前看看 就酱~

本文来自 我的个人博客,原文地址:yejiwei.com/post/85

对于手写题的理解:

手写题其实对面试者的要求会高一点,因为不仅要考我们对于 API 的使用情况,还考我们对原理的理解,而且还要求写出来。所以我这里的建议是:

  1. 首先对于考点要知道是干嘛的,比如让实现一个柯里化函数,如果不知道什么是柯里化那肯定是实现不了的。
  2. 其次要对其的使用了如指掌,比如考 forEach 的实现,很多人都不知道 forEach 第一个参数 callbackFn 其实是有三个参数的,另外 forEach 还有一个可选的 thisArg 参数。
  3. 最后才是对原理的理解,建议可以记思路但不要死记代码哈。
  4. 写完之后可以多检测,想想是否还有一些边界情况值得考虑,会给面试官好的映象。

1. 实现一个 compose 函数

compose 是组合的意思,它的作用故名思议就是将多个函数组合起来调用。

我们可以把 compose 理解为了方便我们连续执行方法,把自己调用传值的过程封装了起来,我们只需要给 compose 函数我们要执行哪些方法,他会自动的执行。

实现:

const add1 = (num) => {
  return num + 1;
};

const mul5 = (num) => {
  return num * 5;
};

const sub8 = (num) => {
  return num - 8;
};

const compose = function (...args) {
  return function (num) {
    return args.reduceRight((res, cb) => cb(res), num);
  };
};

console.log(compose(sub8, mul5, add1)(1));  // 2

上面的例子,我要将一个数加1乘5再减8,可以一个一个函数调用,但是那样会很麻烦,使用 compose 会简单很多。

compose 在函数式编程非常常见,Vue 中就有很多地方使用 compose,学会它有助于我们阅读源码。

注意 compose 是从右往左的顺序执行函数的,下面说的 pipe 函数 是从左往右的顺序执行函数。 pipecompose 都是函数式编程中的基本概念,能让代码变得更好~~

2. 实现一个 pipe 函数

pipe 函数和 compose 函数功能一样,只不过是从左往右执行顺序,只用将上面 reduceRight 方法改为 reduce 即可

const add1 = (num) => {
  return num + 1;
};

const mul5 = (num) => {
  return num * 5;
};

const sub8 = (num) => {
  return num - 8;
};

const compose = function (...args) {
  return function (num) {
    return args.reduce((res, cb) => cb(res), num);
  };
};

console.log(compose(sub8, mul5, add1)(1)); // -34

3. 实现一个 forEach 函数

forEach() 方法能对数组的每个元素执行一次给定的函数。

需要注意的点有,

  1. 挂载到 Array 的原型上
  2. 具有 callBackFn, thisArg 两个参数
  3. callBackFn 是一个函数, 且具有三个参数
  4. 返回 undefined
Array.prototype.myForEach = function (callBackFn, thisArg) {
  if (typeof callBackFn !== "function") {
    throw new Error("callBackFn must be function");
  }

  thisArg = thisArg || this;
  const len = this.length;
  for (let i = 0; i < len; i++) {
    callBackFn.call(thisArg, this[i], i, this);
  }
};

4. 实现一个 map 函数

map() 方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。

需要注意的点有,

  1. 挂载到 Array 的原型上
  2. 具有 callBackFn, thisArg 两个参数
  3. callBackFn 是一个函数, 且具有三个参数
  4. 返回一个新数组,每个元素都是回调函数的返回值
Array.prototype.myMap = function (callbackFn, thisArg) {
  if (typeof callbackFn !== "function") {
    throw new Error("callbackFn must be function");
  }

  const arr = [];
  thisArg = thisArg || this;
  const len = this.length;

  for (let i = 0; i < len; i++) {
    arr.push(callbackFn.call(thisArg, this[i], i, this));
  }
  return arr;
};

5. 实现一个 filter 函数

filter() 方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。

需要注意的点有,

  1. 挂载到 Array 的原型上
  2. 具有 callBackFn, thisArg 两个参数
  3. callBackFn 是一个函数, 且具有三个参数
  4. 返回一个新数组,每个元素都需要通过回调函数测试,且为浅拷贝
Array.prototype.myFilter = function (callbackFn, thisArg) {
  if (typeof callbackFn !== "function") {
    throw new Error("must be function");
  }
  
  const len = this.length;
  thisArg = thisArg || this
  const _newArr = [];
  
  for (let i = 0; i < len; i++) {
    if (callbackFn.call(thisArg, this[i], i, this)) {
      if (typeof this[i] === "object") {
        _newArr.push(Object.create(this[i]));
      } else {
        _newArr.push(this[i]);
      }
    }
  }
  return _newArr;
};

6. 自定义函数:在对象中找出符合规则的属性

这个其实跟 filter 挺像的,只不过一个是在数组中过滤元素,一个是在对象中过滤属性。

需要注意的点有,

  1. 挂载到 Object 的原型上
  2. 具有 callBackFn, thisArg 两个参数
  3. callBackFn 是一个函数, 且具有三个参数
  4. 返回一个新数组,每个元素都需要通过回调函数测试,且为对象的属性名。
Object.prototype.filterProperty = function (callbackFn, thisArg) {
  if (typeof callbackFn !== "function") {
    throw new Error("must be function");
  }

  thisArg = thisArg || this;
  const propArray = [];
  for (let prop in this) {
    if (callbackFn.call(thisArg, prop, this[prop], this)) {
      propArray.push(prop);
    }
  }
  return propArray;
};

7. 实现一个 bind 方法

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

需要注意的点有,

  1. 挂载到 Function 的原型上
  2. 具有 thisArgs 和 原函数所需的其他参数
  3. thisArgs 传递的任何值都需转换为对象
  4. bind 中输入的原函数所需的参数需要在返回函数中能接上,意思就是下面两种方式都要支持
foo.bind(obj,1,2)(3)
foo.bind(obj,1,2,3)
  1. 返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
Function.prototype.myBind = function (thisArgs, ...args1) {
  thisArgs = Object(thisArgs)
  const _self = this;
  // const args1 = Array.prototype.slice.call(arguments, 1);
  return function (...args2) {
    // const args2 = Array.prototype.slice.call(arguments, 1);
    return _self.apply(thisArgs, args1.concat(args2));
  };
};

8. 实现一个 call 方法

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

需要注意的点有,

  1. 挂载到 Function 的原型上
  2. 具有 thisArgs 和 原函数所需的其他参数
  3. 需要判断 thisArgs 是否为 undefined, 如果为 undefined 要他赋值全局的对象。
  4. 返回 函数调用结果
Function.prototype.myCall = function (thisArg, ...args) {
  if (thisArg) {
    thisArg = Object(thisArg);
  } else {
    thisArg = typeof window !== "undefined" ? window : global;
  }

  thisArg._fn = this;

  const result = thisArg._fn(...args);

  delete thisArg._fn;

  return result;
};

9. 实现一个 apply 方法

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。

Function.prototype.myApply = function (thisArg, args) {
  if (thisArg) {
    thisArg = Object(thisArg);
  } else {
    thisArg = typeof window !== "undefined" ? window : global;
  }

  let result;
  if (!args) {
    result = thisArg._fn();
  } else {
    result = thisArg._fn(...args);
  }
  delete thisArg._fn;
  return result;
};

:::note{title="备注"} 虽然这个函数的语法与 call() 几乎相同,但根本区别在于,call() 接受一个参数列表,而 apply() 接受一个参数的单数组。 :::

10. 实现一个能自动柯里化的函数

10.1 什么是柯里化?

  1. 柯里化(英语:Currying)是函数式编程里一个非常重要的概念。
  2. 是把接受多个参数的函数变成接受一个单一参数的函数,并且会返回一个函数,这个返回函数接收余下的参数。
  3. 柯里化声称 "如果你固定某些参数,你将得到接受余下参数的一个函数"

举个例子

下面有两个函数 foobar,他们两个调用的结果都相同, 1 + 2 + 3 + 4 = 10,但是调用的方式却不同。

function foo(a,b,c,d) {
  return a + b + c + d
}

function bar(a) {
  return function(b) {
    return function(c) {
      return function(d) {
        return a + b + c + d
    }
  }
}
foo(1,2,3,4) // 10
bar(1)(2)(3)(4) // 10

将函数foo变成bar函数的过程就叫柯里化

上面的 bar 函数还可以简写成

const bar = a => b => c => d => a + b + c + d

10.2 为什么需要柯里化?

10.2.1 单一职责的原则

  • 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理
  • 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后就在下一个函数中再使用处理后的结果

我们把上面的例子改一下 现在我们需要对函数参数做一些操作,a = a +2 , b = b * 2, c = -c 如果全都写在一个函数中是下面这样的

function add (a,b,c) {
  a = a + 2
  b = b * 2
  c = -c
  return a + b + c
}

而柯里化后

function add(a,b,c) {
  a = a + 2
  return function(b,c) {
    b = b * 2
    return function(c) {
      c = -c
      return a + b + c
    }
  }
}

很明显,柯里化后的函数单一性更强了,比如在最外层函数的逻辑就是对a进行操作,第二层函数就是对b进行操作,最内层就是对c进行操作

这是柯里化的第一个好处:更具有单一性

10.2.2 逻辑的复用

我们简化一下第一个例子,现在需要一个加法函数

function add(a,b){
  return a + b
}
add(5,1)
add(5,2)
add(5,3)
add(5,4)

可以发现每次都是5加上另一个数字,每次都要传5其实是没必要的

  • 柯里化的优化
function add(a,b) {
  // 复用的逻辑
  console.log("+",a)
  return function(b) {
    return a + b
  }
}

const add5 = add(5)
add5(2)
add5(3)
add5(5)

可以看到在外层的函数中的代码被复用了,也可以说是定制化了一个加5的函数

10.3. 最终实现

  • 上面的几个例子都是我们手动写的柯里化函数。
  • 有没有办法写一个函数 传入一个函数自动的返回一个柯里化函数?

(终于到了手写了^_^)

  1. 我们要实现一个函数,这个函数能将普通的函数转换成柯里化函数,所以这个函数的框架就有了。
function currying(fn) {
    function curried(...args) {
    }
    return curried
}
  1. 因为原函数的参数我们不确定,所以需要递归的组合原函数的参数,直到 curried函数的参数 等于原函数fn的参数长度时,结束递归。
function currying(fn) {
  function curried(...args) {
    // 判断当前已接收的参数个数是否与fn函数一致
    // 1.当已经传入的参数数量 大于等于 需要的参数时,就执行函数
    if(args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      // 2.没达到个数时,需要返回一个新的函数,继续来接受参数
      return function curried2(...args2) {
        // 接收到参数后,需要递归调用curried来检查函数的个数是否达到
        return curried.apply(this, [...args, ...args2])
      }
    }
  }
  return curried
}
  1. 测试
function add (a,b,c,d) {
  return a + b + c + d
}

const curryingFn = currying(add);
console.log(add(1, 2, 3, 4)); // 10
console.log(curryingFn(1)(2)(3)(4)); // 10
console.log(curryingFn(1, 2)(3)(4)); // 10
console.log(curryingFn(1, 2, 3)(4)); // 10
console.log(curryingFn(1, 2, 3, 4)); // 10

11. 实现一个防抖和节流函数

这题我会带大家先认识一下防抖和节流,然后在一步一步的实现一下,会有很多个版本,一步一步完善,但其实在实际的时候能基本实现v2版本就差不多了。

11.1 什么是防抖和节流?

11.1.1 认识防抖 debounce 函数

场景:在实际开发中,常常会碰到点击一个按钮请求网络接口的场景,这时用户如果因为手抖多点了几下按钮,就会出现短时间内多次请求接口的情况,实际上这会造成性能的消耗,我们其实只需要监听最后一次的按钮,但是我们并不知道哪一次会是最后一次,就需要做个延时触发的操作,比如这次点击之后的300毫秒内没再点击就视为最后一次。这就是防抖函数使用的场景

总结防抖函数的逻辑

  • 当事件触发时,相应的函数并不会立即触发,而是等待一定的时间;
  • 当事件密集触发时,函数的触发会被频繁的推迟
  • 只有等待了一段时间也没事件触发,才会真正的响应函数

11.1.2 认识节流 throttle 函数

场景:开发中我们会有这样的需求,在鼠标移动的时候做一些监听的逻辑比如发送网络请求,但是我们知道 document.onmousemove 监听鼠标移动事件触发频率是很高的,我们希望按照一定的频率触发,比如3秒请求一次。不管中间document.onmousemove 监听到多少次只执行一次。这就是节流函数的使用场景

总结节流函数的逻辑

  • 当事件触发时,会执行这个事件的响应函数;
  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行;
  • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;

11.2. 实现防抖函数

11.2.1 基本实现v-1

const debounceElement = document.getElementById("debounce");

const handleClick = function (e) {
  console.log("点击了一次");
};

// debounce防抖函数
function debounce(fn, delay) {
  // 定一个定时器对象,保存上一次的定时器
  let timer = null
  // 真正执行的函数
  function _debounce() {
    // 取消上一次的定时器
    if (timer) {
      clearTimeout(timer);
    }
    // 延迟执行
    timer = setTimeout(() => {
      fn()
    }, delay);
  }
  return _debounce;
}

debounceElement.onclick = debounce(handleClick, 300);

11.2.2 this-参数v-2

上面 handleClick 函数有两个问题,一个是 this 指向的是 window,但其实应该指向 debounceElement,还一个问题就是无法传递传递参数。

优化:

const debounceElement = document.getElementById("debounce");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
};

function debounce(fn, delay) {
  let timer = null;
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args) // 改变this指向 传递参数
    }, delay);
  }
  return _debounce;
}

debounceElement.onclick = debounce(handleClick, 300);

11.2.3 可选是否立即执行v-3

有些时候我们想点击按钮的第一次就立即执行,该怎么做呢?

优化:

const debounceElement = document.getElementById("debounce");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
};

// 添加一个immediate参数 选择是否立即调用
function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false; // 是否调用过
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }

    // 如果是第一次调用 立即执行
    if (immediate && !isInvoke) {
      fn.apply(this.args);
      isInvoke = true;
    } else {
      // 如果不是第一次调用 延迟执行 执行完重置isInvoke
      timer = setTimeout(() => {
        fn.apply(this, args);
        isInvoke = false;
      }, delay);
    }
  }
  return _debounce;
}

debounceElement.onclick = debounce(handleClick, 300, true);

11.2.4 取消功能v-4

有些时候我们设置延迟时间很长,在这段时间内想取消之前点击按钮的事件该怎么做呢?

优化:

const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
};

function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false; 
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }

    if (immediate && !isInvoke) {
      fn.apply(this.args);
      isInvoke = true;
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args);
        isInvoke = false;
      }, delay);
    }
  }
	
  // 在_debounce新增一个cancel方法 用来取消定时器
  _debounce.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return _debounce;
}

const debonceClick = debounce(handleClick, 5000, false);
debounceElement.onclick = debonceClick;
cancelElemetnt.onclick = function () {
  console.log("取消了事件");
  debonceClick.cancel();
};

11.2.5 返回值v-5(最终版本)

最后一个问题,上面 handleClick 如果有返回值我们应该怎么接收到呢

优化:用Promise回调

const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
  return "handleClick返回值";
};

function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false;
  function _debounce(...args) {
    return new Promise((resolve, reject) => {
      if (timer) clearTimeout(timer);

      if (immediate && !isInvoke) {
        try {
          const result = fn.apply(this, args);
          isInvoke = true;
          resolve(result); // 正确的回调
        } catch (err) {
          reject(err); // 错误的回调
        }
      } else {
        timer = setTimeout(() => {
          try {
            const result = fn.apply(this, args); 
            isInvoke = false;
            resolve(result); // 正确的回调
          } catch (err) {
            reject(err); // 错误的回调
          }
        }, delay);
      }
    });
  }

  _debounce.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return _debounce;
}

const debonceClick = debounce(handleClick, 300, true);
// 创建一个debonceCallBack用于测试返回的值
const debonceCallBack = function (...args) {
  debonceClick.apply(this, args).then((res) => {
    console.log({ res });
  });
};

debounceElement.onclick = debonceCallBack;
cancelElemetnt.onclick = () => {
  console.log("取消了事件");
  debonceClick.cancel();
};

11.3. 实现节流函数

11.3.1 基本实现v-1

这里说一下最主要的逻辑,只要 这次监听鼠标移动事件处触发的时间减去上次触发的时间大于我们设置的间隔就执行想要执行的操作就行了

nowTimelastTime>intervalnowTime - lastTime > interval

nowTime:这次监听鼠标移动事件处触发的时间

lastTime:监听鼠标移动事件处触发的时间

interval:我们设置的间隔

const handleMove = () => {
  console.log("监听了一次鼠标移动事件");
};

const throttle = function (fn, interval) {
  // 记录当前事件触发的时间
  let nowTime;
  // 记录上次触发的时间
  let lastTime = 0;

  // 事件触发时,真正执行的函数
  function _throttle() {
    // 获取当前触发的时间
    nowTime = new Date().getTime();
    // 当前触发时间减去上次触发时间大于设定间隔
    if (nowTime - lastTime > interval) {
      fn();
      lastTime = nowTime;
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 1000);

11.3.2 this-参数v-2

和防抖一样,上面的代码也会有 this 指向问题 以及 参数传递

优化:

const handleMove = (e) => {
	console.log("监听了一次鼠标移动事件", e, this);
};

const throttle = function (fn, interval) {
  let nowTime;
  let lastTime = 0;

  function _throttle(...args) {
    nowTime = new Date().getTime();
    if (nowTime - lastTime > interval) {
      fn.apply(this, args);
      lastTime = nowTime;
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 1000);

11.3.3 可选是否立即执行v-3

上面的函数第一次默认是立即触发的,如果我们想自己设定第一次是否立即触发该怎么做呢?

优化:

const handleMove = (e) => {
  console.log("监听了一次鼠标移动事件", e, this);
};

const throttle = function (fn, interval, leading = true) {
  let nowTime;
  let lastTime = 0;

  function _throttle(...args) {
    nowTime = new Date().getTime();

    // leading为flase表示不希望立即执行函数 
    // lastTime为0表示函数没执行过
    if (!leading && lastTime === 0) {
      lastTime = nowTime;
    }

    if (nowTime - lastTime > interval) {
      fn.apply(this, args);
      lastTime = nowTime;
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 3000, false);

11.3.4 可选最后一次是否执行v-4(最终版本)

如果最后一次监听的移动事件与上一次执行的时间不到设定的时间间隔,函数是不会执行的,但是有时我们希望无论到没到设定的时间间隔都能执行函数,该怎么做呢?

我们的逻辑是:因为我们不知道哪一次会是最后一次,所以每次都设置一个定时器,定时器的时间间隔是距离下一次执行函数的时间;然后在每次进来都清除上次的定时器。这样就能保证如果这一次是最后一次,那么等到下一次执行函数的时候就必定会执行最后一次设定的定时器。

const handleMove = (e) => {
  console.log("监听了一次鼠标移动事件", e, this);
};

// trailing用来选择最后一次是否执行
const throttle = function (fn,interval,leading = true,trailing = false) {
  let nowTime;
  let lastTime = 0;
  let timer;

  function _throttle(...args) {
    nowTime = new Date().getTime();
    // leading为flase表示不希望立即执行函数
    // lastTime为0表示函数没执行过
    if (!leading && lastTime === 0) {
      lastTime = nowTime;
    }

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (nowTime - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = nowTime;
      return;
    }
		
    // 如果选择了最后一次执行 就设置一个定时器
    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
        lastTime = 0;
      }, interval - (nowTime - lastTime));
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 3000, true, true);

12.实现一个深拷贝

这题会考察面试者对边界情况的考虑,比如深拷贝的对象会有很多类型,你是不是都考虑到了呢?

我的建议是最好能实现到最终版本。一般会考察的重点就是看你有没有解决循环引用的问题。

还有一个问题是

12.1. 基本实现 v-1

// 深拷贝函数,参数是一个待拷贝的对象
function deepClone(originValue) {
  // 如果传入的是null或者不是对象类型, 那么直接返回
  if(originValue == null || typeof originValue !== 'object') {
    return originValue
  }
    
  // 创建一个新对象,递归拷贝属性
  const newObj = {}
  for(const key in originValue) {
    // 不拷贝原型上的
    if(originValue.hasOwnProperty(key)) {
        newObj[key] = deepClone(originValue[key])
    }
  }
  
  return newObj
}

测试代码

const obj = {
  name: 'zhangsan',
  address: {
    city: 'hangzhou'
  }
}

const newObj = deepClone(obj)
obj.address.city = 'beijing'
console.log(newObj) // { name: 'zhangsan', address: { city: 'hangzhou' } }

可以看到obj.address.city改成了'beijing'但是newObj.address.city还是hangzhou。说明已经对属性是对象类型作了深拷贝

12.2. 增加数组类型v-2

function deepClone(originValue) {
  if(originValue == null || typeof originValue !== 'object') {
    return originValue
  }

  // 判断传入的是数组还是对象
  const newObj = Array.isArray(originValue) ? [] : {}
  for(const key in originValue) {
    // 不拷贝原型上的
    if(originValue.hasOwnProterty(key)) {
        newObj[key] = deepClone(originValue[key])
    }
  }

  return newObj
}

测试代码

const obj = {
  name: 'zhangsan',
  address: {
    city: 'hangzhou'
  },
  friends: ['zhangsan', 'lisi']
}


const newObj = deepClone(obj)
obj.address.city = 'beijing'
obj.friends[0] = 'wangwu'
console.log(newObj) 
// {
//  name: 'zhangsan',
//  address: { city: 'hangzhou' },
//  friends: [ 'zhangsan', 'lisi' ]
//}

可以看到obj.friends[0]改成了'wangwu'但是newObj.friends[0]还是zhangsan。说明已经对属性是数组类型作了深拷贝

12.3. 增加函数类型v-3

函数一般认为只使用来进行代码的复用,不需要进行深拷贝,但若实现会一下更好。

function deepClone(originValue) {
  // 判断是否是函数类型
  if (originValue instanceof Function) {
    // 将函数转成字符串
    let str = originValue.toString()
    // 截取函数体内容字符串
    let subStr = str.substring(str.indexOf("{") + 1, str.lastIndexOf("}"))
    // 利用截取函数体内容的字符串和函数的构造器重新生成函数并返回
    return new Function(subStr)
  }

  if(originValue == null || typeof originValue !== 'object') {
    return originValue
  }

  // 判断传入的是数组还是对象
  const newObj = Array.isArray(originValue) ? [] : {}
  for(const key in originValue) {
    // 不拷贝原型上的
    if(originValue.hasOwnProterty(key)) {
        newObj[key] = deepClone(originValue[key])
    }
  }

  return newObj
}

测试

const obj = {
  foo: function () {
    console.log("function");
  },
};

const newObj = deepClone(obj);

console.log(obj.foo === newObj.foo); // false

12.4. 增加Set、Map、Date类型v-4

function deepClone(originValue) {

  // 判断是否是函数类型
  if (originValue instanceof Function) {
    // 将函数转成字符串
    let str = originValue.toString()
    // 截取函数体内容字符串
    let subStr = str.substring(str.indexOf("{"), 1, str.lastIndexOf("}"))
    // 利用截取函数体内容的字符串和函数的构造器重新生成函数并返回
    return new Function(subStr)
  }
  
  // 判断是否是Date类型
  if(originValue instanceof Date) {
    return new Date(originValue.getTime())
  }

  // 判断是否是Set类型(浅拷贝)
  if(originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 判断是否是Map类型(浅拷贝)
  if(originValue instanceof Map) {
    return new Map([...originValue])
  }

  if(originValue == null && typeof originValue !== 'object') {
    return originValue
  }

  const newObj = Array.isArray(originValue) ? [] : {}
  for(const key in originValue) {
    // 不拷贝原型上的
    if(originValue.hasOwnProterty(key)) {
        newObj[key] = deepClone(originValue[key])
    }
  }

  return newObj
}

测试

const obj = {
  name: 'zhangsan',
  address: {
    city: 'hangzhou'
  },
  friends: ['zhangsan', 'lisi'],
  set: new Set([1,2,3]),
  map: new Map([['aaa',111], ['bbb', '222']]),
  createTime: new Date()
}

const newObj = deepClone(obj)
console.log(newObj.set === obj.set)  // false
console.log(newObj.map === obj.map) // false
console.log(newObj.createTime === obj.createTime) // false

12.5. 增加Symbol类型v-5

function deepClone(originValue) {

  // 判断是否是函数类型
  if (originValue instanceof Function) {
    // 将函数转成字符串
    let str = originValue.toString()
    // 截取函数体内容字符串
    let subStr = str.substring(str.indexOf("{"), 1, str.lastIndexOf("}"))
    // 利用截取函数体内容的字符串和函数的构造器重新生成函数并返回
    return new Function(subStr)
  }
  
  // 判断是否是Date类型
  if(originValue instanceof Date) {
    return new Date(originValue.getTime())
  }

  // 判断是否是Set类型(浅拷贝)
  if(originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 判断是否是Map类型(浅拷贝)
  if(originValue instanceof Map) {
    return new Map([...originValue])
  }

  // 判断是否是Smybol类型
  if(typeof originValue === 'symbol') {
    return Symbol(originValue.description)
  }

  if(originValue == null && typeof originValue !== 'object') {
    return originValue
  }

  const newObj = Array.isArray(originValue) ? [] : {}
  for(const key in originValue) {
    newObj[key] = deepClone(originValue[key])
  }

  // 对key是Symbol做处理
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  for(const key of symbolKeys) {
    // 不拷贝原型上的
    if(originValue.hasOwnProterty(key)) {
        newObj[key] = deepClone(originValue[key])
    }
  }

  return newObj
}

测试

const s1 = Symbol('aaa')
const s2 = Symbol('bbb')
const obj = {
  name: 'zhangsan',
  address: {
    city: 'hangzhou'
  },
  friends: ['zhangsan', 'lisi'],
  set: new Set([1,2,3]),
  map: new Map([['aaa',111], ['bbb', '222']]),
  createTime: new Date(),
  eating: function() {
    console.log(this.name + ' is eating')
  },
  s1: s1,
  [s2]: {a: 1}
}

const newObj = deepClone(obj)

console.log(obj.s1 === newObj.s1) // flase
console.log(obj[s2] === newObj[s2]) // false

12.6. 增加循环引用(最终版)

// 参数中设置一个WeakMap用来保存
function deepClone(originValue, map = new WeakMap()) {

  // 判断是否是函数类型
  if (originValue instanceof Function) {
    // 将函数转成字符串
    let str = originValue.toString()
    // 截取函数体内容字符串
    let subStr = str.substring(str.indexOf("{"), 1, str.lastIndexOf("}"))
    // 利用截取函数体内容的字符串和函数的构造器重新生成函数并返回
    return new Function(subStr)
  }
  
  // 判断是否是Date类型
  if(originValue instanceof Date) {
    return new Date(originValue.getTime())
  }

  // 判断是否是Set类型(浅拷贝)
  if(originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 判断是否是Map类型(浅拷贝)
  if(originValue instanceof Map) {
    return new Map([...originValue])
  }

  // 判断是否是Smybol类型
  if(typeof originValue === 'symbol') {
    return Symbol(originValue.description)
  }

  if(originValue == null || typeof originValue !== 'object') {
    return originValue
  }

  // 判断之前是否存过,如果有则直接获取返回
  if(map.has(originValue)) {
    return map.get(originValue)
  }

  const newObj = Array.isArray(originValue) ? [] : {}
  // 创建的newObj放到map里
  map.set(originValue, newObj)
  for(const key in originValue) {
    // 不拷贝原型上的
    if(originValue.hasOwnProterty(key)) {
        newObj[key] = deepClone(originValue[key], map)
    }
  }

  // 对key是Symbol做处理
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  for(const key of symbolKeys) {
    newObj[key] = deepClone(originValue[key], map)
  }

  return newObj
}

测试

const s1 = Symbol('aaa')
const s2 = Symbol('bbb')
const obj = {
  name: 'zhangsan',
  address: {
    city: 'hangzhou'
  },
  friends: ['zhangsan', 'lisi'],
  set: new Set([1,2,3]),
  map: new Map([['aaa',111], ['bbb', '222']]),
  createTime: new Date(),
  eating: function() {
    console.log(this.name + ' is eating')
  },
  s1: s1,
  [s2]: {a: 1}
}

obj.info= obj
const newObj = deepClone(obj)
console.log(newObj)

12.7 MessageChannel + Promise

function deepClone(obj) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    port1.postMessage(obj);
    port2.onmessage = (msg) => {
      resolve(msg.data);
    };
  });
}

const obj = {
  a: { b: 1 },
};

deepClone(obj).then((res) => {
  console.log({ res: res, obj });
  obj.a.b = 2;
  console.log({ res: res, obj });
});

13. 实现一个 new 函数

创建一个空对象obj

  1. 创建一个空对象 obj
  2. 将该对象的原型指向构造函数的原型
  3. 将这个新对象绑定到函数的 this
  4. 执行构造函数,把构造函数的属性添加到新对象
  5. 若无显式返回对象或函数,才返回 obj 对象

function _new(TargetClass, ...args) {
  // 1. 创建空对象,并将新对象的__proto__属性指向构造函数的prototype
  const obj = Object.create(TargetClass.prototype)
  // 2. 将这个新对象绑定到函数的this上,执行构造函数
  const ret =TargetClass.apply(obj, args)
  return typeof ret === "object" ? ret : obj;
}


// 测试 
function Foo(name) {
  this.name = name;
}
Foo.prototype.sayHello = function () {
  console.log("this is " + this.name);
};
const f = _new(Foo, "hello");
f.sayHello(); // this is hello
console.log(f); // Foo { name: 'hello' }

14. 实现继承

14.1. 通过原型链实现继承

// 父类: 公共属性和方法
function Person() {
  this.name = "yjw"
}

// 父类定义一个吃的方法
Person.prototype.eating = function() {
  console.log(this.name + ' is eating')
}

// 子类: 特有属性和方法
function Student() {
  this.sno = '001'
}

// Student的原型对象指向一个Person的实例对象per
const per = new Person()
Student.prototype = per

// 子类定义一个学习的方法
Student.prototype.studying = function() {
  console.log(this.name + ' is studying')
}


const stu = new Student()

console.log(stu)
console.log(stu.name) // stu对象中没有name属性 会去他的原型对象per上找 per对象上有name属性
stu.eating()	// stu对象中没有eating方法 会去他的原型对象per上找per对象上也没eating方法 再往上去per的原型对象上找 per的原型对象上有eating方法
stu.studying()
  • 原型链实现继承的弊端
  1. 通过直接打印对象是看不到这个属性的(继承属性看不到
  2. 属性被多个对象共享,如果这个属性是一个引用类型,会造成问题 (修改引用类型 会互相影响
  3. 不能给 Person 传递参数,因为这个对象是一次性创建的(没办法定制化

14.2. 借用构造函数继承

// 父类: 公共属性和方法
function Person(name) {
  this.name = name
}

// 父类定义一个吃的方法
Person.prototype.eating = function() {
  console.log(this.name + ' is eating')
}

// 子类: 特有属性和方法
function Student(name, sno) {
  // 借用了父类的构造函数
  Person.call(this, name)
  this.sno = sno
}

// Student的原型对象指向一个Person的实例对象per
const per = new Person()
Student.prototype = per

// 子类定义一个学习的方法
Student.prototype.studying = function() {
  console.log(this.name + ' is studying')
}

借用构造函数继承解决了上面的三个问题。但还是不够完美

  • 存在的问题
  1. 会调用两次父类的构造函数
  2. 子类对象的原型对象上会多出没用的属性

14.3. 寄生组合式继承

// 父类: 公共属性和方法
function Person(name) {
  this.name = name
}

// 父类定义一个吃的方法
Person.prototype.eating = function() {
  console.log(this.name + ' is eating')
}

// 子类: 特有属性和方法
function Student(name, sno) {
  // 借用了父类的构造函数
  Person.call(this, name)
  this.sno = sno
}

Student.prototype = Object.create(Person.prototype) // 原型式继承 不用new Person()多调用父类构造函数了
Object.defineProperty(Student.prototype, "constructor", {
  enumerable: false,
  configurable: true,
  writable: true,
  value: Student
}) // 改构造函数名称

// 子类定义一个学习的方法
Student.prototype.studying = function() {
  console.log(this.name + ' is studying')
}

// 上面的 Object.create(Person.prototype) 也可以写成 (兼容性)

Object.setPrototypeOf(Student.prototype, Person.prototype)

// 也可以写成 (兼容性)
function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

Student.prototype = object(Person.prototype)

  • 注意:不要让子类型的原型对象 = 父类型的原型对象,因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改

15. 手写实现一个 Promise 类

15.1. Promise 的结构设计

class MyPromise {
  /**
   * Promise规范定义 构造函数接收一个回调函数executor作为参数
   * 这个executor回调函数又接收两个回调函数作为参数 一个是resolve(成功时回调) 一个是reject(时表示回调)
   */
  constructor(executor) {
    // Promise 三种状态 分别是pending(进行中)、fulfilled(已成功)和rejected(已失败)
    this.status = 'pending' 
    this.value = undefined
    this.reason = undefined

    const resolve = (value) => {
      if(this.status === 'pending') {
        // 调用resolve 状态改为fulfilled
        this.status = 'fulfilled'
        // 保存resolve回调的参数
        this.value = value
      }
    }

    const reject = (reason) => {
      if(this.status === 'pending') {
       // reject 状态改为rejected
        this.status = 'rejected'
        // 保存reject回调的参数
        this.reason = reason
      }
    }
		
    // 传入的回调函数是会直接执行的
    executor(resolve,reject)
  }

}

15.2. then 方法的设计

class MyPromise {
  constructor(executor) {
    this.status = 'pending' 
    this.value = undefined
    this.reason = undefined

    const resolve = (value) => {
      if(this.status === 'pending') {
        // 延迟调用(微任务)
        queueMicrotask(() => {
        	this.status = 'fulfilled'
          this.value = value
          this.onFulfilled && this.onFulfilled(this.value)
        }, 0)
      }
    }

    const reject = (reason) => {
      if(this.status === 'pending') {
        // 延迟调用(微任务)
        queueMicrotask(() => {
        	this.status = 'rejected'
          this.reason = reason
          this.onRejected && this.onRejected(this.reason)
        }, 0)
      }
    }
		
    executor(resolve,reject)
  }

  then(onFulfilled, onRejected) {
    onFulfilled && this.onFulfilled = onFulfilled
    onRejected && this.onRejected = onRejected
  }
}

const promise = new MyPromise((resolve, reject) => {
  resolve('resolve')
  reject('reject')
})


promise.then(res => {
  console.log({res})
}, err => {
  console.log({err})
})

上面 then 方法还有几个点需要优化

  1. 目前不支持多次调用(解决方法:将 then 方法中的回调函数保存到数组中)
  2. 不支持链式调用 (解决方法: then 方法中返回 Promise
  3. 如果 then 方法在 resolve 已经执行后再执行,目前 then 中的方法不能调用 (解决方法:then 方法中做判断,如果调用的时候状态已经确定下来,直接调用)
setTimeout(() => {
	promise.then(res =>{
		console.log({res})
	})
}, 10000)

15.3. then 方法的优化

// 封装一个函数
const execFunctionWithCatchError = (exeFn, value, resolve, reject) => {
  try {
    const result = exeFn(value)
    resolve(result)
  } catch(err) {
    reject(err)
  }
}


class MyPromise {
  constructor(executor) {
    this.status = 'pending' 
    this.value = undefined
    this.reason = undefined
    this.onFulfilledFns = []
    this.onRejectFns = []

    const resolve = (value) => {
      if(this.status === 'pending') {
        queueMicrotask(() => {
          if(this.status !== 'pending') return 
          this.status = 'fulfilled'
          this.value = value
          this.onFulfilledFns.forEach(fn => {
            fn && fn(this.value)
          })
        }, 0)
      }
    }

    const reject = (reason) => {
      if(this.status === 'pending') {
        queueMicrotask(() => {
          if(this.status !== 'pending') return 
          this.status = 'rejected'
          this.reason = reason
          this.onRejectFns.forEach(fn => {
            fn && fn(this.reason)
          })
        }, 0)
      }
    }
		
    try {
      executor(resolve,reject)
    } catch(err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      // 如果在then调用的时候状态已经确定下来,直接调用
      if(this.status === 'fulfilled' && onFulfilled) {
        execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
      }

      // 如果在then调用的时候状态已经确定下来,直接调用
      if(this.status === 'rejected' && onRejected) {
        execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
      }

      if(this.status === 'pending') {
        // 将成功的回调和失败的回调都放到数组中
        if(onFulfilled) this.onFulfilledFns.push(() => {
          execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
        })
        if(onRejected) this.onRejectFns.push(() => {
          execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
        })
      }
    })
  }
}

15.4. catch 方法的实现

catch(onRejected) {
  return this.then(undefined, onRejected)
}

then(onFulfilled, onRejected) {
  // 当onRejected为空时 我们手动抛出一个错误
  onRejected = onRejected || (err => { throw err })
  return new  return new MyPromise((resolve, reject) => {
    	...
  })
}

15.5. finally 方法的实现

finally(onFinally) {
  // 不管成功和失败都调用onFinally
  this.then(onFinally, onFinally)
}

then(onFulfilled, onRejected) {
  // 当onRejected为空时 我们手动抛出一个错误
  onRejected = onRejected || (err => { throw err })
   // 当onFulfilled为空时 将上一个promise的value传下去
  onFulfilled = onFulfilled || (value => value)
  return new  return new MyPromise((resolve, reject) => {
    	...
  })
}

15.6. resolve 和 reject 的实现

static resolve(value) {
    return new MyPromise((resolve) => resolve(value))
}

static reject(reason) {
    return new MyPromise((resolve, reject) => reject(reason))
}

15.7. all 和 allSettled 的实现

  1. all: 参数是一个 promises数组 返回一个 promise

    在所有 promise 执行成功后返回所有 promise 结果的一个数组 有一个promise 失败就 reject

  2. 不管有没有 promise rejected,都返回所有 promise 结果

static all(promises) {
  return new MyPromise((resolve, reject) => {
    const values = []
    promises.forEach(promise =>  {
      promise.then(res => {
        values.push(res)
        if(values.length === promises.length) {
          resolve(values)
        }
      }, err => {
        reject(err)
      })
    })
  })
}

static allSettled(promises) {
  return new MyPromise((resolve, reject) => {
    const result = []
    promises.forEach(promise =>  {
      promise.then(res => {
        result.push({state: 'resolved', value: res})
        if(result.length === promises.length) {
          resolve(result)
        }
      }, err => {
        result.push({state: 'rejected', reason: err})
        if(result.length === promises.length) {
          resolve(result)
        }
      })
    })
  })
}

15.8. race 和 any 的实现

  1. race: 返回一个 promise,只要一个 promise 有结果 立刻返回(竞赛)
  2. any: 必须等到有一个正确的结果才返回, 没有正确的结果会返回一个合并的异常
static race(promises) {
  return new MyPromise((resolve, reject) => {
    promises.forEach(promise =>  {
      promise.then(res => {
        resolve(res)
      }, err => {
        reject(err)
      })
    })
  })
}

static any(promises) {
  return new MyPromise((resolve, reject) => {
    const reasons = []
    promises.forEach(promise =>  {
      promise.then(res => {
        resolve(res)
      }, err => {
        reasons.push(err)
        if(reasons.length === promises.length) {
          reject(reasons)
        }
      })
    })
  })
}

15.9. 总结

  • 构造函数里的逻辑:
  1. 定义状态
  2. 定义 resolvereject 回调
  3. resolve 执行微任务队列:改变状态、获取value、then传入执行成功回调
  4. reject 执行微任务队列:改变状态、获取reason、then传入执行失败回调
  • then 方法的逻辑:
  1. 判断 onFulfilledonRejected 为空给默认值
  2. 返回 Promise resovle/reject 支持链式调用、
  3. 判断之前的 promise 状态是否确定 确定的话 onFufilled/onRejected 直接执行
  4. 添加到数组中 push(() => { 执行onFulfilled/onRejected 直接执行代码 })