前端进阶高薪必看-手写源码
前言
此系列作为笔者之前发过的前端高频面试整理的补充 会比较偏向中高前端面试问题 当然大家都是从新手一路走过来的 感兴趣的朋友们都可以看哈
初衷
我相信不少同学面试的时候最怕的一个环节就是手写代码 大家一定听过这句话talk is cheap, show me the code 没事 此文章不仅包含了前端经典的手写源码面试题 还包含了大量的分析和引导 希望能帮助大家更好的食用(欢迎评论 不定时更新补充题库)
注意
本文所有的手写源码实现都是基于 es6 的 不想用原生去实现原因如下:一方面是网上太多原生实现的方案了 另一方面是我们要面向未来编程 多使用 es6 的特性更加贴合实际工作
1 promise
先思考?
- promise 是什么?
 
异步回调解决方案
- promise 是什么?
 
异步回调解决方案
- promise 如何保证异步执行完了再去执行后面的代码?
 
使用 then 关键字 then 接受两个参数 第一个参数(函数)会在 promise resolve 之后执行 第二个参数(函数)会在 promise reject 之后执行
- 为什么能在异步事件执行完成的回调之后再去触发 then 中的函数?
 
引入事件注册机制(将 then 中的代码注册事件 当异步执行完了之后再去触发事件)
- 怎么保证 promise 链式调用 形如 promise.then().then()
 
每个 then 返回的也是一个 promise 对象
- 怎么知道异步事件执行完毕或者执行失败?
 
需要状态表示
具体实现如下
//这里使用es6 class实现
class Mypromise {
  constructor(fn) {
    // 表示状态
    this.state = "pending";
    // 表示then注册的成功函数
    this.successFun = [];
    // 表示then注册的失败函数
    this.failFun = [];
    let resolve = val => {
      // 保持状态改变不可变(resolve和reject只准触发一种)
      if (this.state !== "pending") return;
      // 成功触发时机  改变状态 同时执行在then注册的回调事件
      this.state = "success";
      // 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里为模拟异步
      setTimeout(() => {
        // 执行当前事件里面所有的注册函数
        this.successFun.forEach(item => item.call(this, val));
      });
    };
    let reject = err => {
      if (this.state !== "pending") return;
      // 失败触发时机  改变状态 同时执行在then注册的回调事件
      this.state = "fail";
      // 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里模拟异步
      setTimeout(() => {
        this.failFun.forEach(item => item.call(this, err));
      });
    };
    // 调用函数
    try {
      fn(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  // 实例方法 then
  then(resolveCallback, rejectCallback) {
    // 判断回调是否是函数
    resolveCallback =
      typeof resolveCallback !== "function" ? v => v : resolveCallback;
    rejectCallback =
      typeof rejectCallback !== "function"
        ? err => {
            throw err;
          }
        : rejectCallback;
    // 为了保持链式调用  继续返回promise
    return new Mypromise((resolve, reject) => {
      // 将回调注册到successFun事件集合里面去
      this.successFun.push(val => {
        try {
          //    执行回调函数
          let x = resolveCallback(val);
          //(最难的一点)
          // 如果回调函数结果是普通值 那么就resolve出去给下一个then链式调用  如果是一个promise对象(代表又是一个异步) 那么调用x的then方法 将resolve和reject传进去 等到x内部的异步 执行完毕的时候(状态完成)就会自动执行传入的resolve 这样就控制了链式调用的顺序
          x instanceof Mypromise ? x.then(resolve, reject) : resolve(x);
        } catch (error) {
          reject(error);
        }
      });
      this.failFun.push(val => {
        try {
          //    执行回调函数
          let x = rejectCallback(val);
          x instanceof Mypromise ? x.then(resolve, reject) : reject(x);
        } catch (error) {
          reject(error);
        }
      });
    });
  }
  //静态方法
  static all(promiseArr) {
    let result = [];
    //声明一个计数器 每一个promise返回就加一
    let count = 0
    return new Mypromise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
        promiseArr[i].then(
          res => {
          //这里不能直接push数组  因为要控制顺序一一对应(感谢评论区指正)
            result[i] = res
            count++
            //只有全部的promise执行成功之后才resolve出去
            if (count === promiseArr.length) {
              resolve(result);
            }
          },
          err => {
            reject(err);
          }
        );
      }
    });
  }
  //静态方法
  static race(promiseArr) {
    return new Mypromise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
        promiseArr[i].then(
          res => {
          //promise数组只要有任何一个promise 状态变更  就可以返回
            resolve(res);
          },
          err => {
            reject(err);
          }
        );
      }
    });
  }
}
// 使用
let promise1 = new Mypromise((resolve, reject) => {
  setTimeout(() => {
    resolve(123);
  }, 2000);
});
let promise2 = new Mypromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1234);
  }, 1000);
});
// Mypromise.all([promise1,promise2]).then(res=>{
//   console.log(res);
// })
// Mypromise.race([promise1, promise2]).then(res => {
//   console.log(res);
// });
promise1
  .then(
    res => {
      console.log(res); //过两秒输出123
      return new Mypromise((resolve, reject) => {
        setTimeout(() => {
          resolve("success");
        }, 1000);
      });
    },
    err => {
      console.log(err);
    }
  )
  .then(
    res => {
      console.log(res); //再过一秒输出success
    },
    err => {
      console.log(err);
    }
  );
扩展:如何取消 promise
先思考?
怎么才能取消已经发起的异步呢?
Promise.race()方法可以用来竞争 Promise 谁的状态先变更就返回谁那么可以借助这个 自己包装一个 假的 promise 与要发起的 promise 来实现
具体实现如下
function wrap(pro) {
  let obj = {};
  // 构造一个新的promise用来竞争
  let p1 = new Promise((resolve, reject) => {
    obj.resolve = resolve;
    obj.reject = reject;
  });
  obj.promise = Promise.race([p1, pro]);
  return obj;
}
let testPro = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(123);
  }, 1000);
});
let wrapPro = wrap(testPro);
wrapPro.promise.then(res => {
  console.log(res);
});
wrapPro.resolve("被拦截了");
2 防抖节流
先思考?
- 防抖和节流区别
 
防抖是 N 秒内函数只会被执行一次,如果 N 秒内再次被触发,则重新计算延迟时间(举个极端的例子 如果 window 滚动事件添加了防抖 2s 执行一次 如果你不停地滚动 永远不停下 那这个回调函数就永远无法执行)
节流是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行(还是滚动事件 如果你一直不停地滚动 那么 2 秒就会执行一次回调)
- 防抖怎么保证
 
事件延迟执行 并且在规定时间内再次触发需要清除 这个很容易就想到了 setTimeout
- 节流怎么保证
 
在单位时间内触发了一次就不再生效了 可以用一个 flag 标志来控制
具体实现如下
// 防抖
function debounce(fn, delay=300) {
  //默认300毫秒
  let timer;
  return function() {
    var args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args); // 改变this指向为调用debounce所指的对象
    }, delay);
  };
}
window.addEventListener(
  "scroll",
  debance(() => {
    console.log(111);
  }, 1000)
);
// 节流
//方法一:设置一个标志
function throttle(fn, delay) {
  let flag = true;
  return () => {
    if (!flag) return;
    flag = false;
    timer = setTimeout(() => {
      fn();
      flag = true;
    }, delay);
  };
}
//方法二:使用时间戳
function throttle(fn, delay) {
  let startTime = new Date();
  return () => {
    let endTime = new Date();
    if (endTime - startTime >= delay) {
      fn();
      startTime = endTime;
    } else {
      return;
    }
  };
}
window.addEventListener(
  "scroll",
  throttle(() => {
    console.log(111);
  }, 1000)
);
防抖节流属于性能优化的一点 更多性能优化扩展请点击 性能优化
3 EventEmitter(发布订阅模式--简单版)
先思考?
- 什么是发布订阅模式
 
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到状态改变的通知
- 怎么实现一对多
 
既然一对多 肯定有一个事件调度中心用来调度事件 订阅者可以注册事件(on)到事件中心 发布者可以发布事件(emit)到调度中心 订阅者也可以取消订阅(off)或者只订阅一次(once)
具体实现如下
// 手写发布订阅模式 EventEmitter
class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 实现订阅
  on(type, callBack) {
    if (!this.events) this.events = Object.create(null);
    if (!this.events[type]) {
      this.events[type] = [callBack];
    } else {
      this.events[type].push(callBack);
    }
  }
  // 删除订阅
  off(type, callBack) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter(item => {
      return item !== callBack;
    });
  }
  // 只执行一次订阅事件
  once(type, callBack) {
    function fn() {
      callBack();
      this.off(type, fn);
    }
    this.on(type, fn);
  }
  // 触发事件
  emit(type, ...rest) {
    this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
  }
}
// 使用如下
const event = new EventEmitter();
const handle = (...rest) => {
  console.log(rest);
};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {
  console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");
4 call、apply、bind
先思考?
- call 用法
 
第一个参数 可以改变调用函数的 this 指向 第二个以及之后的参数为传入的函数的参数
let obj = {
  a: 1
};
function fn(name, age) {
  console.log(this.a); //1
  console.log(name);
  console.log(age);
}
fn.call(obj, "我是 lihua", "18");
- 怎么改变 this 指向呢
 
根据 this 特性 对象的方法调用 那么方法内部的 this 就指向这个对象
let obj = {
  a: 1,
  fn(name, age) {
    console.log(this.a); //1
    console.log(name);
    console.log(age);
  }
};
obj.fn("我是lihua", "18");
- 怎么获取传入的不定参数呢
 
利用 es6 ...args 剩余参数获取方法(rest)
具体实现如下
Function.prototype.myCall = function(context, ...args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this; //this指向调用call的函数
  // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
  return context[fn](...args);
};
// apply原理一致  只是第二个参数是传入的数组
Function.prototype.myApply = function(context, args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this;
  // 执行函数并返回结果
  return context[fn](...args);
};
//测试一下 call 和 apply
let obj = {
  a: 1
};
function fn(name, age) {
  console.log(this.a);
  console.log(name);
  console.log(age);
}
fn.myCall(obj, "我是lihua", "18");
fn.myApply(obj, ["我是lihua", "18"]);
let newFn = fn.myBind(obj, "我是lihua", "18");
newFn();
//bind实现要复杂一点  因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this;
  let _this = this
  //  bind情况要复杂一点
  const result = function (...innerArgs) {
    // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
    // 此时由于new操作符作用  this指向result实例对象  而result又继承自传入的_this 根据原型链知识可得出以下结论
    // this.__proto__ === result.prototype   //this instanceof result =>true
    // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
    if (this instanceof _this === true) {
      // 此时this指向指向result的实例  这时候不需要改变this指向
      this[fn] = _this
      this[fn](...[...args, ...innerArgs]) //这里使用es6的方法让bind支持参数合并
      delete this[fn]
    } else {
      // 如果只是作为普通函数调用  那就很简单了 直接改变this指向为传入的context
      context[fn](...[...args, ...innerArgs]);
      delete context[fn]
    }
  };
  // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  // 实现继承的方式一:  构造一个中间函数来实现继承
  // let noFun = function () { }
  // noFun.prototype = this.prototype
  // result.prototype = new noFun()
  // 实现继承的方式二: 使用Object.create
  result.prototype = Object.create(this.prototype)
  return result
};
//测试一下
function Person(name, age) {
  console.log(name); //'我是参数传进来的name'
  console.log(age); //'我是参数传进来的age'
  console.log(this); //构造函数this指向实例对象
}
// 构造函数原型的方法
Person.prototype.say = function() {
  console.log(123);
}
let obj = {
  objName: '我是obj传进来的name',
  objAge: '我是obj传进来的age'
}
// 普通函数
function normalFun(name, age) {
  console.log(name);   //'我是参数传进来的name'
  console.log(age);   //'我是参数传进来的age'
  console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
  console.log(this.objName); //'我是obj传进来的name'
  console.log(this.objAge); //'我是obj传进来的age'
}
// 先测试作为构造函数调用
// let bindFun = Person.myBind(obj, '我是参数传进来的name')
// let a = new bindFun('我是参数传进来的age')
// a.say() //123
// 再测试作为普通函数调用
let bindFun = normalFun.myBind(obj, '我是参数传进来的name')
 bindFun('我是参数传进来的age')
bind 实现 运用原型链相关知识 如果对 js 原型链和继承不是很熟悉 请点传送门
5 new 操作符
先思考?
- new 用法是什么?
 
从构造函数创造一个实例对象 构造函数的 this 指向为创造的实例函数 并且可以使用构造函数原型属性和方法
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function() {
  console.log(this.age);
};
let p1 = new Person("lihua", 18);
console.log(p1.name);
p1.say();
- 怎么实现 this 指向改变?
 
call apply
- 怎么实现构造函数原型属性和方法的使用
 
原型链 原型继承
具体实现如下
function myNew(fn, ...args) {
  // 1.创造一个实例对象
  let obj = {};
  // 2.生成的实例对象继承构造函数原型
  // 方法一 粗暴的改变指向 完成继承
  obj.__proto__ = fn.prototype;
  // 方法二 利用Object.create实现
  // obj=Object.create(fn.prototype)
  // 3.改变构造函数this指向为实例对象
  let result = fn.call(obj, ...args);
  // 4. 如果构造函数执行的结果返回的是一个对象或者函数,那么返回这个对象或函数
  if ((result && typeof result === "object") || typeof result === "function") {
    return result;
  }
  //不然直接返回boj
  return obj;
}
// 测试一下
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function() {
  console.log(this.age);
};
let p1 = myNew(Person, "lihua", 18);
console.log(p1.name);
console.log(p1);
p1.say();
对原型链深入理解学习 建议看看 传送门
6 instanceof
先思考?
- instanceof 原理?
 
右侧对象的原型对象(prototype )是否在左侧对象的原型链上面
- 怎么遍历左侧对象的原型链是关键点?
 
while(true) 一直遍历 直到原型链的尽头 null 都没有相等就说明不存在 返回 false
具体实现如下
function myInstanceof(left, right) {
  let leftProp = left.__proto__;
  let rightProp = right.prototype;
  // 一直会执行循环  直到函数return
  while (true) {
    // 遍历到了原型链最顶层
    if (leftProp === null) {
      return false;
    }
    if (leftProp === rightProp) {
      return true;
    } else {
      // 遍历赋值__proto__做对比
      leftProp = leftProp.__proto__;
    }
  }
}
// 测试一下
let a = [];
console.log(myInstanceof(a, Array));
7 深拷贝
先思考?
- 什么是深拷贝?
 
js 对引用类型的数据进行复制的时候,深拷贝不会拷贝引用类型的引用,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突
- 怎么样才能全部拷贝?
 
递归遍历 直到数据类型不是引用类型才进行赋值操作
具体实现如下
// 定义一个深拷贝函数  接收目标target参数
function deepClone(target) {
    // 定义一个变量
    let result;
    // 如果当前需要深拷贝的是一个对象的话
    if (typeof target === 'object') {
    // 如果是一个数组的话
        if (Array.isArray(target)) {
            result = []; // 将result赋值为一个数组,并且执行遍历
            for (let i in target) {
                // 递归克隆数组中的每一项
                result.push(deepClone(target[i]))
            }
         // 判断如果当前的值是null的话;直接赋值为null
        } else if(target===null) {
            result = null;
         // 判断如果当前的值是一个RegExp对象的话,直接赋值
        } else if(target.constructor===RegExp){
            result = target;
        }else {
         // 否则是普通对象,直接for in循环,递归赋值对象的所有值
            result = {};
            for (let i in target) {
                result[i] = deepClone(target[i]);
            }
        }
     // 如果不是对象的话,就是基本数据类型,那么直接赋值
    } else {
        result = target;
    }
     // 返回最终结果
    return result;
}
扩展:利用JSON的方法实现简单的深拷贝
let targetObj = JSON.parse(JSON.stringify(sourceObj))复制代码
但是它有局限性
- 不可以拷贝 undefined , function, RegExp 等等类型的
 - 会抛弃对象的 constructor,所有的构造函数会指向 Object
 - 对象有循环引用,会报错
 
                    
                
                
            
        
浙公网安备 33010602011771号