基本类型和引用类型
ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。
目前基本类型有:Boolean、Null、Undefined、Number、String、Symbol(ES6),引用类型有:Object、Array、Function。
深拷贝与浅拷贝
我们先看两个简单的案例:
//案例1 var num1 = 1, num2 = num1; console.log(num1) //1 console.log(num2) //1 num2 = 2; //修改num2 console.log(num1) //1 console.log(num2) //2 //案例2 var obj1 = {x: 1, y: 2}, obj2 = obj1; console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 1, y: 2} obj2.x = 2; //修改obj2.x console.log(obj1) //{x: 2, y: 2} console.log(obj2) //{x: 2, y: 2}
案例1中的值就为基本类型,案例2中的值就为引用类型。案例2中的赋值,会改变之前对象的就是典型的浅拷贝,而不会改变之前对象的就是深拷贝,并且深拷贝与浅拷贝的概念只存在于引用类型。
Array自有方法
slice()方法:
//一维数组: var arr1 = [1, 2], arr2 = arr1.slice(); console.log(arr1); //[1, 2] console.log(arr2); //[1, 2] arr2[0] = 3; //修改arr2 console.log(arr1); //[1, 2] console.log(arr2); //[3, 2] //二维数组: var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice(); console.log(arr1); //[1, 2, [3, 4]] console.log(arr2); //[1, 2, [3, 4]] arr2[2][1] = 5; console.log(arr1); //[1, 2, [3, 5]] console.log(arr2); //[1, 2, [3, 5]]
可以看到,一维数组arr2的修改没有影响到arr1,而二维数组影响到了,所以slice()方法只能实现一维数组的深拷贝。
具备同等特性的还有:concat、Array.from() 。
Object自有方法
1.Object.assign():
//一维对象 var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1); console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 1, y: 2} obj2.x = 2; //修改obj2.x console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 2, y: 2} //二维对象 var obj1 = { x: 1, y: { m: 1 } }; var obj2 = Object.assign({}, obj1); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 2}} console.log(obj2) //{x: 2, y: {m: 2}}
可以看出Object.assign()也只能实现一维对象的深拷贝。
2.JSON.parse(JSON.stringify(obj))
var obj1 = { x: 1, y: { m: 1 } }; var obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 2, y: {m: 2}}
看起来JSON.parse(JSON.stringify(obj))可以完美的实现深拷贝,但是查询MDN文档后会发现有这样的一段描述:
undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
什么意思呢,我们把刚刚上面那个例子改一下:
var obj1 = { x: 1, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; var obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)} console.log(JSON.stringify(obj1)); //{"x":1} console.log(obj2) //{x: 1}
可以看到,y,z,a都被忽略了,验证了MDN文档那句话的意思:JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象。不过JSON.parse(JSON.stringify(obj))简单粗暴,已经满足90%的使用场景了。
递归解决
我们发现JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题,我们可以自己使用递归写一个方法实现深拷贝:
function deepCopy(obj){ let result = {}, keys = Object.keys(obj), key = null, temp = null; keys.map(function(item,index){ key = keys[index]; temp = obj[key]; if(temp && typeof temp === 'object'){ // 如果字段的值也是一个对象则递归操作 result[key] = deepCopy(temp); }else{ result[key] = temp; } }); return result; } var obj1 = { x: { m: 1 }, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; var obj2 = deepCopy(obj1); obj2.x.m = 2; console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)} console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}
这个递归函数可以完美解决上面那些问题,我们也可以用第三方库:jquery的$.extend和lodash的_.cloneDeep来解决深拷贝。上面虽然是用Object来验证的,但对于Array也同样适用,因为Array也是特殊的Object。