all方法 手写promise_前端进阶高薪必看手写源码篇
前言
此系列作为笔者之前发过的前端高频面试整理的补充 会比较偏向中高前端面试问题 当然大家都是从新手一路走过来的 感兴趣的朋友们都可以看哈
初衷
我相信不少同学面试的时候最怕的一个环节就是手写代码 大家一定听过这句话 talk is cheap, show me the code 没事 此文章不仅包含了前端经典的手写源码面试题 还包含了大量的分析和引导 希望能帮助大家更好的食用(欢迎评论 不定时更新补充题库)
注意
本文所有的手写源码实现都是基于 es6 的 不想用原生去实现原因如下:一方面是网上太多原生实现的方案了 另一方面是我们要面向未来编程 多使用 es6 的特性更加贴合实际工作
1 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;// 为了保持链式调用 继续返回promisereturn 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 = 0return 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); //过两秒输出123return 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)
具体实现如下
// 手写发布订阅模式 EventEmitterclass 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); //1console.log(name);console.log(age);}fn.call(obj, "我是 lihua", "18");复制代码
- 怎么改变 this 指向呢
根据 this 特性 对象的方法调用 那么方法内部的 this 就指向这个对象
let obj = {a: 1, fn(name, age) {console.log(this.a); //1console.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 和 applylet 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 =>trueif (this instanceof _this === true) {// 此时this指向指向result的实例 这时候不需要改变this指向this[fn] = _thisthis[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的第一个参数 也就是例子中的objconsole.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; }//不然直接返回bojreturn 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;// 一直会执行循环 直到函数returnwhile (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
对象有循环引用,会报错
源自:https://juejin.im/post/5eb8f5cdf265da7bd44254b4
声明:文章著作权归作者所有,如有侵权,请联系小编删除。
感谢 · 转发欢迎大家留言
all方法 手写promise_前端进阶高薪必看手写源码篇相关推荐
- 前端为什么有的接口明明是成功回调却执行了.catch失败回调_前端进阶高薪必看-手写源码篇(高频技术点)...
前言 此系列作为笔者之前发过的前端高频面试整理的补充 会比较偏向中高前端面试问题 当然大家都是从新手一路走过来的 感兴趣的朋友们都可以看哈 初衷 我相信不少同学面试的时候最怕的一个环节就是手写代码 大 ...
- 手撸Spring系列2:IOC/DI 思想(源码篇-IOC)
说在前头: 笔者本人为大三在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正. ...
- 前端精品课程免费看,写课评赢心动大礼!
新的一年新的开始,今天学院小编特为大家带来新的福利,前端精品课程免费看,写课评还有大礼拿.看看视频,动动手指,参与活动就有现金券及保温杯等丰富礼品可以领,还在等什么,快随小编一起来吧~ 先来看看课程背 ...
- 一步一步手绘Spring AOP运行时序图(Spring AOP 源码分析)
相关内容: 架构师系列内容:架构师学习笔记(持续更新) 一步一步手绘Spring IOC运行时序图一(Spring 核心容器 IOC初始化过程) 一步一步手绘Spring IOC运行时序图二(基于XM ...
- 手撸Spring系列12:MyBatis(源码篇)
说在前头: 笔者本人为大三在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正. ...
- android毕业设计——基于Android+Java+Python的手机端办公自动化OA系统设计与实现(毕业论文+程序源码)——办公自动化OA系统
基于Android+Java+Python的手机端办公自动化OA系统设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于Android+Java+Python的手机端办公自动化OA系统设计与实 ...
- 个人设计web前端大作业~响应式游戏网站源码(HTML+CSS+Bootstrap)
HTML期末大作业课程设计游戏主题html5网页~响应式游戏网站(HTML+CSS+Bootstrap) ~个人设计web前端大作业 临近期末, 你还在为HTML网页设计结课作业,老师的作业要求感到头 ...
- spring-boot-2.0.3启动源码篇二 - run方法(一)之SpringApplicationRunListener
前言 Springboot启动源码系列还只写了一篇,已经过去一周,又到了每周一更的时间了(是不是很熟悉?),大家有没有很期待了?我会尽量保证启动源码系列每周一更,争取不让大家每周的期望落空.一周之中可 ...
- 25个Web前端开发工程师必看的国外大牛和酷站
逛了一周国外大牛们的博客与酷站,真是满满的钦佩.震撼.羡慕.惊喜---- Web设计是一个不断变化的领域,因此掌握最新的发展趋势及技术动向对设计师来说非常重要.无论是学习新技术,还是寻找免费资源与工具 ...
最新文章
- 使用insert向表中添加数据MySQL_使用INSERT语句向表中插入数据(MSSQLSERVER版)
- 网络技术学习资料分享
- 使用Elastic APM监控你的.NET Core应用
- SQL语句inner join,left join ,right join连接的不同之处
- Django model select的各种用法详解
- 二进制文件和ASCII文件有何差别
- 第三部分 SOA项目的运维
- 设置嵌入式系统开机自动启动程序
- 如何建语料库_语料库-如何建设语料?如何建设语料库 爱问知识人
- 画出计算机网络中两级子网,计算机网络基础练习题
- word参考文献后面空格太大
- 用Python回忆QQ空间里的青春
- matlab mcc-m,【matlab】matlab中 mcc、mbuild和mex命令详解
- 南部龙凤小学:六一文艺表演
- 原生滑动选择器 html,html选择器
- npm安装报错(npm ERR! code EPERM npm ERR! syscall mkdir npm ERR! path C:\Program Files\nodejs\node_ca...)
- 翻译考试用计算机作答,上半年CATTI考试方式还是纸笔,下半年就实行全面机考?真是几家欢喜几家愁!...
- incident用法_“我出事故了”书到用时方恨少,事故用“incident”还是“accident”?...
- 小程序生成带信息的二维码
- idea解析文件部分乱码及其idea 设置编码
热门文章
- java outputstream api,Java8 stream API以及常用方法
- JS在HTML中放的位置
- react 更新input 默认值setfieldsvalue_值得收藏的React知识点查漏补缺
- qt qlabel 布局重叠_Pyqt5布局管理实例
- C++基础与深度解析第七章:深入IO
- ubuntu查看python安装路径
- php文件上传格式限制,如何在PHP中限制文件上传类型的文件大小?
- verdi中波形怎么看间距_小间距led显示屏金线封装真伪怎么看?
- 绝地求生2月19服务器维护,绝地求生2月19日停机维护几点结束_2020绝地求生2月19日开服时间介绍_求知软件网...
- mysql 中文排序_mysql如何按照中文排序解决方案