首页 文章 精选 留言 我的

精选列表

搜索[前端语言],共10000篇文章
优秀的个人博客,低调大师

前端阿里面试—— 实现一个深拷贝

前言 深拷贝这个功能在开发中经常使用到,特别在对引用类型的数据进行操作时,一般会先深拷贝一份赋值给一个变量,然后在对其操作,防止影响到其它使用该数据的地方。 如何实现一个深拷贝,在面试中出现频率一直居高不下。因为在实现一个深拷贝过程中,可以看出应聘者很多方面的能力。 本专栏将从青铜到王者来介绍怎么实现一个深拷贝,以及每个段位对应的能力。 青铜段位 JSON.parse(JSON.stringify(data)) 复制代码 这种写法非常简单,而且可以应对大部分的应用场景,但是它有很大缺陷的。如果你不知道它有那些缺陷,而且这种实现方法体现不出你任何能力,所以这种实现方法处于青铜段位。 如果对象中存在循环引用的情况也无法正确实现深拷贝。 const a = { b: 1, } a.c = a; JSON.parse(JSON.stringify(a)); 如果data里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象。 const a = { b: new Date(1536627600000), } console.log(JSON.parse(JSON.stringify(a))) 如果data里有RegExp、Error对象,则序列化的结果将只得到空对象; const a = { b: new RegExp(/\d/), c: new Error('错误') } console.log(JSON.parse(JSON.stringify(a))) 如果data里有函数,undefined,则序列化的结果会把函数置为undefined或丢失; const a = { b: function (){ console.log(1) }, c:1, d:undefined } console.log(JSON.parse(JSON.stringify(a))) 如果data里有NaN、Infinity和-Infinity,则序列化的结果会变成null const a = { b: NaN, c: 1.7976931348623157E+10308, d: -1.7976931348623157E+10308, } console.log(JSON.parse(JSON.stringify(a))) 白银段位 深拷贝的核心就是对引用类型的数据的拷贝处理。 function deepClone(target){ if(target !== null && typeof target === 'object'){ let result = {} for (let k in target){ if (target.hasOwnProperty(k)) { result[k] = deepClone(target[k]) } } return result; }else{ return target; } } 以上代码中,deepClone函数的参数target是要深拷贝的数据。 执行target !== null && typeof target === 'object'判断target是不是引用类型。 若不是,直接返回target。 若是,创建一个变量result作为深拷贝的结果,遍历target,执行deepClone(target[k])把target每个属性的值深拷贝后赋值到深拷贝的结果对应的属性result[k]上,遍历完毕后返回result。 在执行deepClone(target[k])中,又会对target[k]进行类型判断,重复上述流程,形成了一个递归调用deepClone函数的过程。就可以层层遍历要拷贝的数据,不管要拷贝的数据有多少子属性,只要子属性的值的类型是引用类型,就会调用deepClone函数将其深拷贝后赋值到深拷贝的结果对应的属性上。 另外使用for...in循环遍历对象的属性时,其原型链上的所有属性都将被访问,如果只要只遍历对象自身的属性,而不遍历继承于原型链上的属性,要使用hasOwnProperty方法过滤一下。 在这里可以向面试官展示你的三个编程能力。 对原始类型和引用类型数据的判断能力。 对递归思维的应用的能力。 深入理解for...in的用法。 黄金段位 白银段位的代码中只考虑到了引用类型的数据是对象的情况,漏了对引用类型的数据是数组的情况。 function deepClone(target){ if(target !== null && typeof target === 'object'){ let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {}; for (let k in target){ if (target.hasOwnProperty(k)) { result[k] = deepClone(target[k]) } } return result; }else{ return target; } } 以上代码中,只是额外增加对参数target是否是数组的判断。执行Object.prototype.toString.call(target) === "[object Array]"判断target是不是数组,若是数组,变量result为[],若不是数组,变量result为{}。 在这里可以向面试官展示你的两个编程能力。 正确理解引用类型概念的能力。 精确判断数据类型的能力。 铂金段位 假设要深拷贝以下数据data let data = { a: 1 }; data.f=data 执行deepClone(data),会发现控制台报错,错误信息如下所示。 这是因为递归进入死循环导致栈内存溢出了。根本原因是data数据存在循环引用,即对象的属性间接或直接的引用了自身。 function deepClone(target) { function clone(target, map) { if (target !== null && typeof target === 'object') { let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {}; if (map[target]) { return map[target]; } map[target] = result; for (let k in target) { if (target.hasOwnProperty(k)) { result[k] = deepClone(target[k]) } } return result; } else { return target; } } let map = {} const result = clone(target, map); map = null; return result } 以上代码中利用额外的变量map来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去map中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。最后需要把变量map置为null,释放内存,防止内存泄露。 在这里可以向面试官展示你的两个编程能力。 对循环引用的理解,如何解决循环引用引起的问题的能力。 对内存泄露的认识和避免泄露的能力。 砖石段位 该段位要考虑性能问题了。在上面的代码中,我们遍历数组和对象都使用了for...in这种方式,实际上for...in在遍历时效率是非常低的,故用效率比较高的while来遍历。 function deepClone(target) { /** * 遍历数据处理函数 * @array 要处理的数据 * @callback 回调函数,接收两个参数 value 每一项的值 index 每一项的下标或者key。 */ function handleWhile(array, callback) { const length = array.length; let index = -1; while (++index < length) { callback(array[index], index) } } function clone(target, map) { if (target !== null && typeof target === 'object') { let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {}; if (map[target]) { return map[target]; } map[target] = result; const keys = Object.prototype.toString.call(target) === "[object Array]" ? undefined : Object.keys( target); function callback(value, key) { if (keys) { // 如果keys存在则说明value是一个对象的key,不存在则说明key就是数组的下标。 key = value; } result[key] = clone(target[key], map) } handleWhile(keys || target, callback) return result; } else { return target; } } let map = {} const result = clone(target, map); map = null; return result } 用while遍历的深拷贝记为deepClone,把用for ... in遍历的深拷贝记为deepClone1。利用console.time()和console.timeEnd()来计算执行时间。 let arr = []; for (let i = 0; i < 1000000; i++) { arr.push(i) } let data = { a: arr }; console.time(); const result = deepClone(data); console.timeEnd(); console.time(); const result1 = deepClone1(data); console.timeEnd(); 从上图明显可以看到用while遍历的深拷贝的性能远优于用for ... in遍历的深拷贝。 在这里可以向面试官展示你的四个编程能力。 具有优化代码运行性能的能力。 了解遍历的效率的能力。 了解++i和i++的区别。 代码抽象的能力。 星耀段位 在这个阶段应该考虑代码逻辑的严谨性。在上面段位的代码虽然已经满足平时开发的需求,但是还是有几处逻辑不严谨的地方。 判断数据不是引用类型时就直接返回target,但是原始类型中还有 Symbol 这一特殊类型的数据,因为其每个 Symbol 都是独一无二,需要额外拷贝处理,不能直接返回。 判断数据是不是引用类型时不严谨,漏了typeof target === function'的判断。 只考虑了 Array、Object 两种引用类型数据的处理,引用类型的数据还有Function 函数、Date 日期、RegExp 正则、Map 数据结构、Set 数据机构,其中 Map 、Set 属于 ES6 的。 废话不多说,直接贴上全部代码,代码中有注释。 function deepClone(target) { // 获取数据类型 function getType(target) { return Object.prototype.toString.call(target) } //判断数据是不是引用类型 function isObject(target) { return target !== null && (typeof target === 'object' || typeof target === 'function'); } //处理不需要遍历的应引用类型数据 function handleOherData(target) { const type = getType(target); switch (type) { case "[object Date]": return new Date(target) case "[object RegExp]": return cloneReg(target) case "[object Function]": return cloneFunction(target) } } //拷贝Symbol类型数据 function cloneSymbol(targe) { const a = String(targe); //把Symbol字符串化 const b = a.substring(7, a.length - 1); //取出Symbol()的参数 return Symbol(b); //用原先的Symbol()的参数创建一个新的Symbol } //拷贝正则类型数据 function cloneReg(target) { const reFlags = /\w*$/; const result = new target.constructor(target.source, reFlags.exec(target)); result.lastIndex = target.lastIndex; return result; } //拷贝函数 function cloneFunction(targe) { //匹配函数体的正则 const bodyReg = /(?<={)(.|\n)+(?=})/m; //匹配函数参数的正则 const paramReg = /(?<=\().+(?=\)\s+{)/; const targeString = targe.toString(); //利用prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的 if (targe.prototype) { //普通函数 const param = paramReg.exec(targeString); const body = bodyReg.exec(targeString); if (body) { if (param) { const paramArr = param[0].split(','); //使用 new Function 重新构造一个新的函数 return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } else { return null; } } else { //箭头函数 //eval和函数字符串来重新生成一个箭头函数 return eval(targeString); } } /** * 遍历数据处理函数 * @array 要处理的数据 * @callback 回调函数,接收两个参数 value 每一项的值 index 每一项的下标或者key。 */ function handleWhile(array, callback) { let index = -1; const length = array.length; while (++index < length) { callback(array[index], index); } } function clone(target, map) { if (isObject(target)) { let result = null; if (getType(target) === "[object Array]") { result = [] } else if (getType(target) === "[object Object]") { result = {} } else if (getType(target) === "[object Map]") { result = new Map(); } else if (getType(target) === "[object Set]") { result = new Set(); } //解决循环引用 if (map[target]) { return map[target]; } map[target] = result; if (getType(target) === "[object Map]") { target.forEach((value, key) => { result.set(key, clone(value, map)); }); return result; } else if (getType(target) === "[object Set]") { target.forEach(value => { result.add(clone(value, map)); }); return result; } else if (getType(target) === "[object Object]" || getType(target) === "[object Array]") { const keys = getType(target) === "[object Array]" ? undefined : Object.keys(target); function callback(value, key) { if (keys) { // 如果keys存在则说明value是一个对象的key,不存在则说明key就是数组的下标。 key = value } result[key] = clone(target[key], map) } handleWhile(keys || target, callback) } else { result = handleOherData(target) } return result; } else { if (getType(target) === "[object Symbol]") { return cloneSymbol(target) } else { return target; } } } let map = {} const result = clone(target, map); map = null; return result } 在这里可以向面试官展示你的六个编程能力。 代码逻辑的严谨性。 深入了解数据类型的能力。 JS Api 的熟练使用的能力。 了解箭头函数和普通函数的区别。 熟练使用正则表达式的能力。 模块化开发的能力 王者段位 以上代码中还有很多数据类型的拷贝,没有实现,有兴趣的话可以在评论中实现一下,王者属于你哦! 总结 综上所述,面试官叫你实现一个深拷贝,其实是要考察你各方面的能力。例如 白银段位 对原始类型和引用类型数据的判断能力。 对递归思维的应用的能力。 黄金段位 正确理解引用类型概念的能力。 精确判断数据类型的能力。 铂金段位 对循环引用的理解,如何解决循环引用引起的问题的能力。 对内存泄露的认识和避免泄露的能力。 砖石段位 具有优化代码运行性能的能力。 了解遍历的效率的能力。 了解++i和i++的区别。 代码抽象的能力。 星耀段位 代码逻辑的严谨性。 深入了解数据类型的能力。 JS Api 的熟练使用的能力。 了解箭头函数和普通函数的区别。 熟练使用正则表达式的能力。 模块化开发的能力 所以不要去死记硬背一些手写代码的面试题,最好自己动手写一下,看看自己达到那个段位了。 最后 对于大厂面试,我最后想要强调的一点就是心态真的很重要,是决定你在面试过程中发挥的关键,若不能正常发挥,很可能就因为一个小失误与offer失之交臂,所以一定要重视起来。另外提醒一点,充分复习,是消除你紧张的心理状态的关键,但你复习充分了,自然面试过程中就要有底气得多。 我平时一直有整理面试题的习惯,有随时跳出舒适圈的准备,不知不觉整理了229页了,在这里分享给大家,有需要的点击这里免费领取题目+解析PDF 篇幅有限,仅展示部分内容 如果你需要这份完整版的面试题+解析,【点击我】就可以了。 希望大家明年的金三银四面试顺利,拿下自己心仪的offer!

优秀的个人博客,低调大师

React新Context API在前端状态管理的实践

11/10号文档资料已全面更新!;《【阿里P7】Android高级教程+BAT面试题》,可复制链接后用石墨文档 App 或小程序打开:【阿里P7】移动互联网架构师进阶高级教程+BAT面试题众所周知,React的单向数据流模式导致状态只能一级一级的由父组件传递到子组件,在大中型应用中较为繁琐不好管理,通常我们需要使用Redux来帮助我们进行管理,然而随着React 16.3的发布,新context api成为了新的选择。 一、Redux的简介以及缺陷 Redux来源于Flux并借鉴了Elm的思想,主要原理如下图所示: 可以看到,Redux的数据流其实非常简单,外部事件通过actionCreator函数调用dipsatch发布action到reducers中,然后各自的reducer根据action的类型(action.type) 来按需更新整个应用的state。 redux设计有以下几个要点: 1. state是单例模式且不可变的,单例模式避免了不同store之间的数据交换的复杂性,而不可变数据提供了十分快捷的撤销重做、“时光旅行”等功能。 2. state只能通过reducer来更新,不可以直接修改。 3. reducer必须是纯函数,形如(state,action) => newState redux本身是个非常纯粹的状态管理库,需要通过react-redux这个库的帮助来管理react的状态。react-redux主要包含两个部分。 1. Provider组件:可以将store注入到子组件的cotext中,所以一般放在应用的最顶层。 2. connect函数: 返回一个高阶函数,把context中由Provider注入的store取出来然后通过props传递到子组件中,这样子组件就能顺利获取到store了。 虽然redux在React项目中得到了普遍的认可与使用率,然而在现实项目中redux还是存在着很多缺点: 1.样板代码过多:增加一个action往往需要同时定义相应的actionType然后再写N个相关的reducer。例如当添加一个异步加载事件时,需要同时定义加载中、加载失败以及加载完成三个actionType,需要一个相对应的reducer通过switch分支来处理对应的actionType,冗余代码过多。 2.更新效率问题:由于使用不可变数据模式,每次更新state都需要拷贝一份完整的state造成了内存的浪费以及性能的损耗。 3.数据传递效率问题:由于react-redux采用的旧版context API,context的传递存在着效率问题。 其中,第一个问题目前已经存在着非常多的解决方案,诸如dva、rematch以及mirror等等,笔者也造过一个类似的轮子restated这里不做过多阐述。 第二个问题首先redux以及react-redux中已经做了非常详尽的优化了,其次擅用shouldComponentUpdate方法也可以避免很多不必要的更新,最后,也可以使用一些不可变数据结构如immutable、Immr等来从根本上解决拷贝开销问题。 第三个问题属于React自身API的局限,从第三方库的角度上来说,能做的很有限。 二、Context API context API主要用来解决跨组件传参泛滥的问题(prop drilling),旧的context API的语法形式如下: 可以看到,使用context api可以把DeliverComponent中的参数color直接跨越MidComponent传递到ReceiverComponent中,不需要冗余的使用props参数传递,特别是ReceiverComponent层级特别深的时候,使用context api能够很大程度上节省重复代码避免bug。 旧Context API的缺陷 旧的context api主要存在如下的缺陷: 1.代码冗余:提供context的组件要定义childContextTypes与getChildContext才能把context传下去。同时接收context的也要先定义contextTypes才能正确拿到数据。 2.传递效率:虽然功能上context可以跨层级传递,但是本质上context也是同props一样一层一层的往下传递的,当层级过深的时候还是会出现效率问题。 3.shouldComponentUpdate:由于context的传递也是一层一层传递,因此它也会受到shouldComponent的阻断。换句话说,当传递组件的context变化时,如果其下面某一个中间组件的shouldComponentUpdate方法返回false,那么之后的接收组件将不会受到任何context变化。 为了解决旧版本的shouldComponentUpdate问题,保证所有的组件都能收到store的变化,react-redux只能传递一个getState方法给各个组件用于获取最新的state(直接传递state可能会被阻断,后面的组件将接收不到state的变化),然后每个connect组件都需要直接或间接监听state的变化,当state发生改变时,通过内部notifyNestedSubs方法从上往下依次触发各个子组件通过getState方法获取最新的state更新视图。这种方式效率较低而且比较hack。 三、新Context API React自16.3开始提供了一个新的context api,彻底解决了旧Context API存在的种种问题。 下面是新context api(右)与使用旧context api的react-redux(左)数据流的比较: 可以看到,新的context api可以直接将context数据传递到传递到子组件中而不需要像旧context api那样级联传递。因此也可以突破shouldComponentUpdate的限制。新版的context api的定义如下: 下面是一个比较简单的应用示例: 可以看到新的context api主要包含一个Provider和Consumer对,在Provider输入的数据可以在Consumer中获得。 新context api的要点如下: 1. Provider和 Consumer必须来自同一次 React.createContext调用。也就是说 NameContext.Provider和 AgeContext.Consumer是无法搭配使用的。 2. React.createContext方法接收一个默认值作为参数。当 Consumer外层没有对应的 Provider时就会使用该默认值。 3. Provider 组件的 valueprop 值发生变更时,其内部组件树中对应的 Consumer组件会接收到新值并重新执行 children函数。此过程不受 shouldComponentUpdete 方法的影响。 4. Provider组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is和 === 的行为不完全相同。 5. Consumer组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。 四、新Context API的应用 新的Context API大大简化了react状态传递的问题,也出现了一些基于它的状态管理库,诸如:unstated、react-waterfall等等。下面我们主要尝试使用新context api来造一个react-redux的轮子。 1. Provider 由于新的context api传递过程中不会被shouldComponentUpdate阻断,所以我们只需要在Provider里面监听store变化即可: 2. connect 相比较于react-redux,connect中的高阶组件逻辑就简单的多,不需要监听store变化,直接获得Provider传入的state然后再传递给子组件即可: 好了,至此整个React-redux的接口和功能都已经基本cover了,下面继续介绍一些比较重要的性能优化。 3. 性能优化 - 减少重复渲染 性能优化最大的一部分就是要减少无意义的重复渲染,当WrappedComponent的参数值没有变化时我们应该阻止其重新渲染。可以通过手写shouldComponentUpdate方法实现,也可以直接通过PureComponent组件来达到我们的目标: 这里需要注意的是,本示例的mapDispatchToProps未支持ownProps参数,因此可以把它的返回值看成是不变的,否则每次调用它返回的action函数都是新创建的,从而导致Prevent接收到的参数始终是不同的,达不到预期效果。更为复杂的情况请参考react-redux源码中selector相关的部分。 4. 性能优化 - 减少层级嵌套 性能优化另一个要点就是减少组件的层级嵌套,新context api在获取context值的时候需要嵌套一层Consumer组件,这也是其比旧context api劣势的地方。除此之外,我们应该尽量减少层级的嵌套。因此在前一个性能优化中我们不应该再次嵌套一个PureComponent,取而代之的是,我们可以直接在Cunsumer中实现一个memory机制,实现代码如下: 下面是前后chrome开发人员工具中组件层级的对比,可以看到嵌套层级成功减少了一层,两层嵌套是新context api的局限,如果要保持react-redux的接口模式则无法再精简了。 原文链接https://www.toutiao.com/i6615135777976222216/ 最后 我们今年整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。 主要包括腾讯,以及字节跳动,华为,小米,等一线互联网公司主流架构技术。如果你有需要,尽管拿走好了。 以下为我整理的资料免费分享;【阿里P7】Android高级教程+BAT面试题 1.Android高级技术脑图 2.P7级Android高级架构视频教程 3.Android核心高级技术PDF文档+BAT大厂面试真题解析 4.Android思维脑图(技能树) 1.Android高级技术脑图; 查漏补缺,体系化深入学习提升 2.【Android高级架构视频教程】; 全套部分展示; java与Android内核进阶专题视频与源码 阿里P7级全套高级学习视频; 3.Android核心高级技术PDF文档,BAT大厂面试真题解析 4.Android思维脑图(技能树) 免费分享 【阿里P7】移动互联网架构师进阶高级教程+BAT面试题 为什么免费分享? 我的目的是让更多需要的Android开发朋友能够提升自己的技术水平无论是Android,还是qq,微信,360等,想在互联网上最大程度推广,就必须免费!如果我的学习资料对你有帮助,点个赞,谢谢!

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册