深拷贝的几个误区
周末赋闲在家,因为太冷了,不想出门,索性宅一天好了。但是闲着没事做总是很无聊的,正好新的一年想抓一下童鞋同学的代码质量,就随便打开了几个童鞋写的代码。于是故事就展开了。
团队大了之后,如何统一团队代码风格其实是一个蛮重要的问题,目前我们团队使用lint的方式进行了限制,这次的review可以说是初见成效,除了不少同学偷偷摸摸的通过noverify的方式提交代码以外。不过没看多久就发现了一段有趣的代码:
function deepClone(obj, res = {}) { const _res = res; for(let key in obj) { if (obj[key] == obj) { continue; } if (typeof obj[key] === 'object') { if(Array.isArray(obj[key])){ _res[key] = obj[key].slice(); } else { _res[key] = deepClone(obj[key], _res[key]); } } else { _res[key] = obj[key]; } } return _res; }
初看上去就会好奇,为什么不使用lodash现成的深拷贝呢?一看是个h5的项目,推测可能是为了整体包的大小做了取舍,也无可厚非吧。不过仔细看代码,乍一看好像还挺好,还细心的考虑的数组的情况,但是再仔细看得时候又觉得好像有什么地方不对,如果入参是个字符串感情你给别人返回一个空对象么。。跑了一个case发现果然有点问题:
var c = { a:1 }; var d = new Map(); d.set('a', 1); var a = { a: 1, b: true, c: ()=>{console.log(123)}, d: [1,2], e: d, f: c, g: {} } var b = deepClone(a);
1、虽然正常的处理好像都没有什么问题,但是遇到新的数据结构如Map的时候,这种拷贝就会出问题;
2、另外,它只处理了单层循环引用的情况,多层的时候情况会更复杂;
3、而且这样递归,层级一深还会有爆栈的隐患,相当的不安全...
4、虽然它对单独处理的数组的拷贝,但如果数组的某项的值是一个对象,它这样的处理依然有问题...
所以深拷贝究竟应该怎么写呢?
本着能google不手写的原则,查了下网络,好的写法没发现几个,倒是几个误区经有的文章经常会提到且一笔略过:
1、JSON.parse(JSON.stringify(obj)) 的实现究竟算不算深拷贝?
当然算,但是这种实现有几个潜在风险:
1)它的原理是将能够JSON化的值JSON化,再重新生成一个新的JSON对象。所以它能够实现的基础是这个值是能够被JSON化的,像诸如function、map、set全是不能JSON化的,一转就没了。
2) 它还有一个风险是在处理循环引用时是会报错的。这点很多童鞋在实操的时候特别容易忽略,特别是在node端进行端端通信的时候,曾经一个报错查半天,真的是血的教训。
所以,如果是纯JSON的数据的深拷贝且不包含循环引用,是可以使用这个方法的
2、递归在js中是有风险的
常见的实现都是基于递归的,但是递归本身在js的runtime,很容易因为层级过深而导致爆栈。
而通常的方式则是通过“拍平”树级结构的对象成一个数组,来进行拷贝。
3、深拷贝的情况因业务场景的定义会有不同
有些业务场景需要保持拷贝对象中的值的引用关系不变,而有些却要改变。另外,js的数据结构发展到今天,需要在拷贝时处理的边界情况已经很多了,你需要好好考虑清楚哪些情况需要怎么处理。
其实话说回来,仔细看得话,你会发现第一种写法和jQuery中extend的方式其实是很像的,通常的情况也基本能覆盖了,不过在现在这个语境下,相比比较“安全”的实现深拷贝,还是建议使用lodash的cloneDeep(相比jQuery和underscore深拷贝大概60行左右的代码,lodash使用了近几百行代码,考虑了各种边界情况,也可谓是业界楷模了)