前言
深拷贝这个性能在开发中常常应用到,特地在对援用类型的数据进行操作时,个别会先深拷贝一份赋值给一个变量,而后在对其操作,避免影响到其它应用该数据的中央。
如何实现一个深拷贝,在面试中呈现频率始终居高不下。因为在实现一个深拷贝过程中,能够看出应聘者很多方面的能力。
本专栏将从青铜到王者来介绍怎么实现一个深拷贝,以及每个段位对应的能力。
青铜段位
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!