javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS常见手写题

JavaScript常见手写题超全汇总

作者:前端要努力QAQ

作为前端开发,JS是重中之重,下面这篇文章主要给大家介绍了关于JavaScript常见手写题的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

前言

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

对于手写题的理解:

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

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() 方法能对数组的每个元素执行一次给定的函数。

需要注意的点有,

Array.prototype.myForEach = function (callBackFn, thisArg) {
  if (typeof callBackFn !== "function") {
    throw new Error("callBackFn must be function");
  }

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

4. 实现一个 map 函数

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

需要注意的点有,

Array.prototype.myMap = function (callbackFn, thisArg) {
  if (typeof callbackFn !== "function") {
    throw new Error("callbackFn must be function");
  }

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

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

5. 实现一个 filter 函数

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

需要注意的点有,

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, thisArg[i], i, thisArg)) {
      if (typeof thisArg[i] === "object") {
        _newArr.push(Object.create(thisArg[i]));
      } else {
        _newArr.push(thisArg[i]);
      }
    }
  }
  return _newArr;
};

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

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

需要注意的点有,

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 thisArg) {
    if (callbackFn.call(thisArg, prop, thisArg[prop], thisArg)) {
      propArray.push(prop);
    }
  }
  return propArray;
};

7. 实现一个 bind 方法

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

需要注意的点有,

foo.bind(obj,1,2)(3)
foo.bind(obj,1,2,3)
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 值和单独给出的一个或多个参数来调用一个函数。

需要注意的点有,

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 什么是柯里化?

举个例子

下面有两个函数 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. 最终实现

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

function currying(fn) {
    function curried(...args) {
    }
    return curried
}
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
}
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);

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);

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);

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();
};

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

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

nowTime−lastTime>intervalnowTime - lastTime > 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.hasOwnProterty(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) {
    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) {
    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) {
    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) {
    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) {
    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)

13. 实现一个 new 函数

创建一个空对象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()

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')
}

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

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.prototyp)

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 方法还有几个点需要优化

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 的实现

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 的实现

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. 总结

总结

到此这篇关于JavaScript常见手写题超全汇总的文章就介绍到这了,更多相关JS常见手写题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文