拷贝即复制。
本文只讨论js中复杂数据类型的复制问题(Object,Array等),不讨论基本数据类型(null,undefined,string,number和boolean),这些类型的值本身就存储在栈内存中(string类型的实际值还是存储在堆内存中的,但是js把string当做基本类型来处理 ),不存在引用值的情况。
浅复制和深复制都可在已有对象的基础上再生一份,但对象的实例存储在堆内存中然后通过一个引用值去操作对象,由此复制的时候就存在两种情况了:复制引用和复制实例,这也是浅复制和深复制的区别所在。
- 浅复制:浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。
- 深复制:深复制不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深复制的对象的引用不包含任何原有对象或对象上的任何对象,复制后的对象与原来的对象是完全隔离的。
浅复制:
浅复制就是简单的引用复制,示例如下:
var src = {
name:"src";
}
var target = src; //复制一份src对象的引用
target.name = "target"; //改变复制后对象的name值
console.log(src.name); //输出target,即原对象也发生了改变,证明复制的是指向堆内存内对象位置的引用
Array的slice()和concat()方法都会返回一个新的数组实例,这与直接引用复制不同,但数组中的对象元素(Object、Array等)仍只是复制了引用,并不是真正的深复制。
var array = [1, [1, 2, 3], { name: "array" }];
var array_concat = array.concat();//copy array返回新数组
var array_slice = array.slice(0);//copy array返回新数组
array_concat[0] = 3//改变基本类型值不会相互影响
array_concat[1][0] = 5; //改变array_concat中数组元素的值
array_slice[0] = 4//改变基本类型值不会相互影响
array_slice[2].name = "array_slice"; //改变引用类型值会相互影响
console.log(array[1]); //[5,2,3]//改变引用类型值会相互影响
console.log(array_slice[1]); //[5,2,3]//改变引用类型值会相互影响
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice
console.log(array)//[ 1, [ 5, 2, 3 ], { name: 'array_slice' } ]
console.log(array_concat)//[ 3, [ 5, 2, 3 ], { name: 'array_slice' } ]
console.log(array_slice)//[ 4, [ 5, 2, 3 ], { name: 'array_slice' } ]
深复制:
由深复制的定义来看,深复制要求如果源对象存在对象属性,那么需要进行递归复制,从而保证复制的对象与源对象完全隔离。然而还有一种处在浅复制和深复制范围之间的复制——jQuery的extend方法在deep参数为false时所谓的“浅复制”。这种复制只进行一个层级的复制:即如果源对象中存在对象属性,那么复制的对象上也会引用相同的对象。这不符合深复制的要求,但又比简单的复制引用的复制程度有了加深。
- 递归实现
function deepCopy(oldObj, newObj) {
var newObj = newObj || {}
for (var i in oldObj) {
if (oldObj[i] instanceof Object) {
if (oldObj[i].constructor === Array) {
newObj[i] = []
} else {
newObj[i] = {}
}
deepCopy(oldObj[i], newObj[i])
} else {
newObj[i] = oldObj[i]
}
}
return newObj
}
var obj1 = {
country: 'China',
city: ['Beijing,Shanghai,Nanjing'],
age: 16,
friends: {
name: 'dot',
sex: 'female',
age: null
}
}
var obj2 = {
name: 'dolby',
fav: 'food'
}
var obj3 = null
console.log(deepCopy(obj1, obj2))
//{ name: 'dolby',
// fav: 'food',
// country: 'China',
// city: [ 'Beijing,Shanghai,Nanjing' ],
// age: 16,
// friends: { name: 'dot', sex: 'female', age: null } }
console.log(deepCopy(obj1, obj3))
//{ country: 'China',
// city: [ 'Beijing,Shanghai,Nanjing' ],
// age: 16,
// friends: { name: 'dot', sex: 'female', age: null } }
- JSON对象的parse()和stringify()方法
stringify()方法将JS对象序列化成JSON字符串,parse()方法将JSON字符串反序列化成JS对象,借助这两个方法可以实现对象的深复制。
var obj1 = {
country: 'China',
city: ['Beijing', 'Shanghai', 'Nanjing'],
age: 16,
friends: {
name: 'dot',
sex: 'female',
age: null
}
}
var deepCopyObj = JSON.parse(JSON.stringify(obj1))
console.log(deepCopyObj)
//{ country: 'China',
// city: [ 'Beijing', 'Shanghai', 'Nanjing' ],
// age: 16,
// friends: { name: 'dot', sex: 'female', age: null } }
deepCopyObj.age = 0
deepCopyObj.city[1] = 'tianjin'
console.log(deepCopyObj)
//{ country: 'China',
// city: [ 'Beijing', 'tianjin', 'Nanjing' ],
// age: 0,
// friends: { name: 'dot', sex: 'female', age: null }
console.log(obj1)
//{ country: 'China',
// city: [ 'Beijing', 'Shanghai', 'Nanjing' ],
// age: 16,
// friends: { name: 'dot', sex: 'female', age: null } }
从代码输出看出,复制后的deepCopyObj与obj1完全隔离,二者不会相互影响。
这个方法可满足基本的深复制需求,而且能够处理JSON格式能表示的所有数据类型(即 Number, String, Boolean, Array, 扁平对象),但对于正则表达式类型、函数类型等无法进行深复制(而且会直接丢失相应的值),同时如果对象中存在循环引用的情况也无法正确处理。
- jQuery库中的extend复制方法
在 jQuery 中可通过添加一个参数来实现递归extend。调用$.extend(true, {}, ...)就可以实现深复制。jQuery 无法正确深复制 JSON 对象以外的对象.
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = $.extend({}, x), //浅复制
z = $.extend(true, {}, x); //深复制
y.b.f === x.b.f // true
z.b.f === x.b.f // false
- lodash库的.clone() 或 .cloneDeep()方法——拥抱未来的库
lodash中关于复制的方法有两个,分别是.clone()和.cloneDeep()。其中.clone(obj, true)等价于.cloneDeep(obj)。
var $ = require("jquery"),
_ = require("lodash");
var arr = new Int16Array(5),
obj = { a: arr },
obj2;
arr[0] = 5;
arr[1] = 6;
// 1. jQuery
obj2 = $.extend(true, {}, obj);
console.log(obj2.a); // [5, 6, 0, 0, 0]
Object.prototype.toString.call(obj2); // [object Int16Array]
obj2.a[0] = 100;
console.log(obj); // [100, 6, 0, 0, 0]
//此处jQuery不能正确处理Int16Array的深复制!!!
// 2. lodash
obj2 = _.cloneDeep(obj);
console.log(obj2.a); // [5, 6, 0, 0, 0]
Object.prototype.toString.call(arr2); // [object Int16Array]
obj2.a[0] = 100;
console.log(obj); // [5, 6, 0, 0, 0]
注意:如果对象比较大,层级比较多,深复制会带来性能上的问题,在遇到需要采用深复制的场景时可以考虑看看有没有其他替代的方案,在实际应用场景中也是浅复制更为常用。
参考资料:
- 深入剖析 JavaScript 的深复制
- js浅复制和深复制