从层次上来看,对象的复制可以简单地分为浅复制和深复制,顾名思义,浅复制是指只复制一层对象的属性,不会复制对象中的对象的属性,对象的深复制会复制对象中层层嵌套的对象的属性。
在复制对象时,除了要复制对象的属性外,还要兼顾到是否保留了对象的constructor属性,是否对每一种数据类型(Javascript常见的数据类型有String,Number,Boolean,Data,RegExp,Array,Funtion,Object)都实现正确的复制。项目中,我们可以根据实际情况,决定需要实现什么样程度的复制。
本文是我在复制对象方面的一些心得总结,由浅复制到深复制,由只复制简单属性到复制Function,RegExp等复杂属性,层层递进。如有陈述不当之处,烦请指出,不胜感激。
浅复制
浅复制只会依次复制对象的每一个属性,不会对这些属性进行递归复制。下面是一个简单的浅复制实现。
//对象浅复制
function shadowCopy(obj){if(typeof obj !== 'object') return obj;for(var prop in obj){if(obj.hasOwnProperty(prop)){newObj[prop] = obj[prop];}}return newObj;}
仔细观察,不难发现上述方法的缺陷:
1.不能正确实现数组的浅复制
2.复制操作丢失了对象的constructor属性
好,我们现在已经发现了问题所在,只需针对性地解决,一个还算完美的浅复制对象的方法就诞生了!
//对象浅复制function shadowCopy(obj){if(typeof obj !== 'object') return ;var newObj;//保留对象的constructor属性if(obj.constructor === Array){newObj = [];} else {newObj = {};newObj.constructor = obj.constructor;}for(var prop in obj){if(obj.hasOwnProperty(prop)){newObj[prop] = obj[prop];}}return newObj;}
浏览器中测试一下:
var arr1 = [0,1,2];console.log(arr1);console.log(shadowCopy(arr1));var arr2 = [0,1,2,[3,4,5]],arr2Copy = shadowCopy(arr2);console.log(arr2);console.log(arr2Copy);arr2Copy[3][0] = 6;console.log(arr2[3][0]); //6
Good! 可以正确实现数组复制和并且保留constructor了,但细心的你一定发现了,浅复制后的对象的 arr2Copy[3]
和 arr2[3]
指向的是一个对象,改变其中一个,同时也会改变另一个。我们想要实现的是 复制,但这并不是复制呀!
这是浅复制的一个弊端所在,接下让我们看看深复制是怎样解决这个问题的。
深复制
深复制需要层层递归,复制对象的所有属性,包括对象属性的属性的属性....(晕~)
如果只是需要简单地复制对象的属性,而不用考虑它的constructor,也不用考虑函数,正则,Data等特殊数据类型,那这里有一个深复制的小trick,两行代码即可:
function deepCopy(obj){if(typeof obj !== "object"){ return ;}var str = JSON.stringify(obj);return JSON.parse(str);
}
大多数情况下,上面的就可以满足要求了,但一些时候,我们需要把函数,正则等特殊数据类型也考虑在内,或者当前环境不支持JSON时,上面的方法也就不适用了。这时,我们可以通过递归来实现对象的深层复制,如下:
function deepCopy(obj){if(typeof obj !== "object"){ return ;}var newObj;//保留对象的constructor属性if(obj.constructor === Array){newObj = [];} else {newObj = {};newObj.constructor = obj.constructor;}for(var prop in obj){if(typeof obj[prop] === 'object'){if(obj[prop].constructor === RegExp ||obj[prop].constructor === Date){newObj[prop] = obj[prop];} else {//递归newObj[prop] = deepCopy(obj[prop]);}} else {newObj[prop] = obj[prop];}}return newObj;
}
function deepCopy(p, c) {var c = c || {};for(var i in p) {if(typeof p[i] === 'object') {c[i] = (p[i].constructor === Array) ? [] : {};deepCopy(p[i], c[i]);} else {c[i] = p[i];}}return c;}console.log(deepCopy({a: [1, 2],b: 3}));console.log(deepCopy([{a: 'a'}, 2]));
先用上面的例子测试:
棒!可以正确实现多维数组的复制,再看是否能实现函数和正则的复制:
function Person(name){this.name = name;this.age = age;this.search = new RegExp(name);this.say = function(){console.log(this.name + "今年" + this.age + "岁了");}
}
var p1 = new Person("Claiyre",20),p2 = deepCopy(p1);console.log(p1);
console.log(p2);p2.age = 22;
p1.say();
p2.say();
圆满完成!!
稍加整理,我们就可以得到一个较为通用的js对象复制函数:
function deepCopy(obj){var newObj = obj.constructor === Array ? []:{};newObj.constructor = obj.constructor;if(typeof obj !== "object"){ return ;} else if(window.JSON){//若需要考虑特殊的数据类型,如正则,函数等,需把这个else if去掉即可newObj = JSON.parse(JSON.stringify(obj));} else {for(var prop in obj){if(obj[prop].constructor === RegExp ||obj[prop].constructor === Date){newObj[prop] = obj[prop];} else if(typeof obj[prop] === 'object'){//递归newObj[prop] = deepCopy(obj[prop]);} else {newObj[prop] = obj[prop];}}} return newObj;
}
前言
拷贝也是面试经典呐!
数组的浅拷贝
如果是数组,我们可以利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。
比如:
var arr = ['old', 1, true, null, undefined];var new_arr = arr.concat();new_arr[0] = 'new';console.log(arr) // ["old", 1, true, null, undefined]
console.log(new_arr) // ["new", 1, true, null, undefined]
用 slice 可以这样做:
var new_arr = arr.slice();
但是如果数组嵌套了对象或者数组的话,比如:
var arr = [{old: 'old'}, ['old']];var new_arr = arr.concat();arr[0].old = 'new';
arr[1][0] = 'new';console.log(arr) // [{old: 'new'}, ['new']]
console.log(new_arr) // [{old: 'new'}, ['new']]
我们会发现,无论是新数组还是旧数组都发生了变化,也就是说使用 concat 方法,克隆的并不彻底。
如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或者数组,就会只拷贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化。
我们把这种复制引用的拷贝方法称之为浅拷贝,与之对应的就是深拷贝,深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。
所以我们可以看出使用 concat 和 slice 是一种浅拷贝。
数组的深拷贝
那如何深拷贝一个数组呢?这里介绍一个技巧,不仅适用于数组还适用于对象!那就是:
var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]var new_arr = JSON.parse( JSON.stringify(arr) );console.log(new_arr);
是一个简单粗暴的好方法,就是有一个问题,不能拷贝函数,我们做个试验:
var arr = [function(){console.log(a)
}, {b: function(){console.log(b)}
}]var new_arr = JSON.parse(JSON.stringify(arr));console.log(new_arr);
我们会发现 new_arr 变成了:
不能拷贝函数
浅拷贝的实现
以上三个方法 concat、slice、JSON.stringify 都算是技巧类,可以根据实际项目情况选择使用,接下来我们思考下如何实现一个对象或者数组的浅拷贝。
想一想,好像很简单,遍历对象,然后把属性和属性值都放在一个新的对象不就好了~
嗯,就是这么简单,注意几个小点就可以了:
var shallowCopy = function(obj) {// 只拷贝对象if (typeof obj !== 'object') return;// 根据obj的类型判断是新建一个数组还是对象var newObj = obj instanceof Array ? [] : {};// 遍历obj,并且判断是obj的属性才拷贝for (var key in obj) {if (obj.hasOwnProperty(key)) {newObj[key] = obj[key];}}return newObj;
}
深拷贝的实现
那如何实现一个深拷贝呢?说起来也好简单,我们在拷贝的时候判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数不就好了~
var deepCopy = function(obj) {if (typeof obj !== 'object') return;var newObj = obj instanceof Array ? [] : {};for (var key in obj) {if (obj.hasOwnProperty(key)) {newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];}}return newObj;
}
性能问题
尽管使用深拷贝会完全的克隆一个新对象,不会产生副作用,但是深拷贝因为使用递归,性能会不如浅拷贝,在开发中,还是要根据实际情况进行选择。
下期预告
难道到这里就结束了?是的。然而本篇实际上是一个铺垫,我们真正要看的是 jquery 的 extend 函数的实现,下一篇,我们会讲一讲如何从零实现一个 jquery 的 extend 函数。
专题系列
Javascript专题系列目录地址:github.com/mqyqingfeng…。
Javascript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。