在我不长的开发经历中,常常在js中遇到数组或对象拷贝的情况,总是不能写对。 于是决定好好梳理一遍相关的知识。
预备知识: 引用类型
ECMAScript 变量可能包含两种不同数据类型的值: 基本类型值和引用类型值 。
当从一个变量向另一个变量复制引用类型的值时,实际复制的是指针,指向存储在堆中的同一个对象。因此改变其中一个变量,就会影响另一个变量。
——《JavaScript高级程序设计(第三版)》
数组Array和对象Object同属于引用类型,因此在拷贝时需要额外的工作,而不能简单的按照 c=a , a=b, b=c的方式来赋值。
数组拷贝
数组的浅拷贝就是直接赋值
var a = [1,2,3];
var b = a; // b = [1,2,3]
b[0] = 10; // b = [10,2,3], a = [10,2,3]
引用类型传递的是地址,因此修改数组b后数组a也会跟着改变。
-
改进办法1 Array.splice( ) 方法
数组类型的splice( )方法本来时用于切割数组的,接收两个参数,起点与终点索引值,将这个区间内的数组元素<b>浅拷贝</b>到一个新数组并返回,原数组保持不变。 若不传递任何参数给splice( )方法,则会默认<b>浅拷贝</b>数组中的每一个元素到一个新数组并返回。由于是复制的数组元素而非数组变量本身,因此传递的是数组中每一个元素的值或地址。
var a = [1,2,3]; var b = a.splic(); //b = [1,2,3] b[0] = 10; //b = [10,2,3], a = [1,2,3]
这个办法具有局限性,因为其终究是浅拷贝,如果数组中的元素含有引用类型,那么改变新数组中它的值仍会反应到原数组中:
var obj = { prop: 1}; var a = [obj,1,2,3]; // a = [{prop:1},1,2,3] var b = a.splice(); // b = [{prop:1},1,2,3] b[0].prop = 20; b[1] = 10; // b = [{prop:20},10,2,3] // a = [{prop:20},1,2,3]
另外,当被拷贝数组中包含数组元素时(类似二维数组),使用该方法复制将返回空数组。
-
改进办法2 Array . concat( ) 方法
数组类型的concat( ) 方法主要用于连接多个数组返回一个新数组,若不带参数则返回一个原数组的拷贝,遗憾的是这个拷贝仍为浅拷贝。与splice( ) 方法表现稍不同的是,若被复制数组中包含数组元素,它仍将正常工作,返回浅拷贝。
var a = [1,2,3]; var b = [4,5]; var c = [a,b]; //c = [[1,2,3],[4,5]]; var d = c.concat(); //d = [[1,2,3],[4,5]]; var e = c.splice(); //e = []; d[1][1] = 10; //d = [[1,2,3],[4,10]]; //c = [[1,2,3],[4,10]]
对象拷贝
直接赋值仍然是浅拷贝,要做到对象的深拷贝,需要将属性一一拷贝到新对象上:
var a = {prop1:1, prop2:2, prop3:3};
//声明一个函数执行深拷贝
function deepCopy(obj){ //接收待拷贝的对象最为参数
var copy = {}; //声明新副本对象
for(prop in obj){ // for in语句遍历对象属性
copy[prop] = obj[prop] // for in 语句中遍历结果为属性的字符串名,因此需以[]方式访问
}
return copy; //返回新对象
}
var b = deepCopy(a); // b = {prop1:1,prop2:2,prop3:3}
b.prop1 = 10; // b = {prop1:10,prop2:2,prop3:3}
// a = {prop1:1, prop2:2,prop3:3}
上述这总解决办法只能拷贝所有属性均为基本类型值的对象,如果拷贝的对象其属性中包含了Object或Array等引用类型,仍然会出现指向同一个地址的情况(虽然对象本身没有指向同一个地址了,但是这些引用类型属性仍然指向了同一个地址)。考虑到对象属性层层嵌套的情况并不少见,因此deepCopy( )函数仍然需要优化:
function deepCopy(obj) {
let copy = {};
for (prop in obj) {
if (obj[prop] instanceof Object) { //先判断该属性是否也是一个Object对象
copy[prop] = deepCopy(obj[prop]); //若为对象则对该属性再递归调用deepCopy()赋值给新对象的该属性
} else {
copy[prop] = obj[prop] //若该属性不为Object类型,则直接赋值即可
}
}
return copy; //返回拷贝出的新对象
}
到这里,上述这些方法函数已经完全能够解决我遇到的所有情景了。但其实这个问题还可以推得很深推得很细,下面就来做点"无用功", 再往前迈进一步。
混合拷贝
如前所述,数组的拷贝方法在数组元素为Object或者Array类型时仍然有无法做到副本与原变量独立。 再加上JavaScript “独特”的语言特性,数组与对象可以做到 “你中有我,我中有你” 的混合状态,因此可以再次改进deepCopy( ) ,让它能实现混合情景下的深拷贝。
function deepCopy(origin){
let copy = null; //声明副本
if(origin.constructor === Array){ //确定副本的类型
copy = [];
}else if(origin.constructor === Object){
copy = {};
}
for(i in origin){ //遍历原变量的枚举属性
if(origin[i] instanceof Object){ //Array和Object类型其原型链上均含Object构造函数
copy[i] = deepCopy(origin[i])
}else{
copy[i] = origin[i];
}
}
return copy;
}
现在,deepCopy( ) 可以轻松应对任何包含 数组或者对象的深拷贝了。
虽然 MDN不推荐用for in遍历数组,因为其返回的是索引数字的字符串值,并且在不同的ECMA-262标准中返回的属性或索引值顺序是不同的,不一定按照对象构造时的属性顺序或数组索引值的大小顺序。但是这里拷贝数组则不用担心,虽然不按顺序返回索引值,但最终还是遍历了整个数组。 这样对象分支和数组分支就合并在了同一个for in语句中,代码变得更为简洁。
参考网站: MDN-JavaScript
网友评论