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

青铜段位


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),会发现控制台报错,错误信息如下所示。 image

这是因为递归进入死循环导致栈内存溢出了。根本原因是 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) {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
类型数据

function cloneReg(target) {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-Yylan4S3-1715893112874)]

[外链图片转存中…(img-xGbIrYPk-1715893112875)]

[外链图片转存中…(img-MorPCwvX-1715893112875)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值