工做中常常会遇到须要复制 JavaScript 数据的时候,遇到 bug 时实在使人头疼;面试中也常常会被问到如何实现一个数据的深浅拷贝,可是你对其中的原理清晰吗?一块儿来看一下吧!
想要更加透彻的理解为何 JavaScript 会有深浅拷贝,须要先了解下 JavaScript 的数据类型有哪些,通常分为基本类型(Number、String、Null、Undefined、Boolean、Symbol )和引用类型(对象、数组、函数)。jquery
基本类型是不可变的,任何方法都没法改变一个基本类型的值,也不能够给基本类型添加属性或者方法。可是能够为引用类型添加属性和方法,也能够删除其属性和方法。面试
基本类型和引用类型在内存中的存储方式也大不相同,基本类型保存在栈内存中,而引用类型保存在堆内存中。为何要分两种保存方式呢? 由于保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,可是咱们能够把它的地址写在栈内存中以供咱们访问。json
说来这么多,咱们来看个示例:api
let num1 = 10; let obj1 = { name: "hh" } let num2 = num1; let obj2 = obj1; num2 = 20; obj2.name = "kk"; console.log(num1); // 10 console.log(obj1.name); // kk
执行完这段代码,内存空间里是这样的:数组
能够看到 obj1 和 obj2 都保存了一个指向该对象的指针,全部的操做都是对该引用的操做,因此对 obj2 的修改会影响 obj1。数据结构
小结:函数
之因此会出现深浅拷贝,是因为 JS 对 基本类型和 引用类型的处理不一样。 基本类型指的是简单的数据段,而 引用类型指的是一个对象保存在堆内存中的地址,JS 不容许咱们直接操做内存中的地址,也就是说不能操做对象的内存空间,因此,咱们对对象的操做都只是在操做它的引用而已。在复制时也是同样,若是咱们复制一个基本类型的值时,会建立一个新值,并把它保存在新的变量的位置上。而若是咱们复制一个引用类型时,一样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并非对象自己,而是指向该对象的指针。因此咱们复制引用类型后,两个变量其实指向同一个对象,因此改变其中的一个对象,会影响到另一个。spa
浅拷贝只是复制基本类型的数据或者指向某个对象的指针,而不是复制对象自己,源对象和目标对象共享同一块内存;若对目标对象进行修改,存在源对象被篡改的可能。指针
咱们来看下浅拷贝的实现:code
/* sourceObj 表示源对象 * 执行完函数,返回目标对象 */ function shadowClone (sourceObj = {}) { let targetObj = Array.isArray(sourceObj) ? [] : {}; let copy; for (var key in sourceObj) { copy = sourceObj[key]; targetObj[key] = copy; } return targetObj; }
// 定义 source let sourceObj = { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr1'}, 1], func: () => 'sourceFunc1', obj: { string: 'obj1', func: () => 'objFunc1' } } // 拷贝sourceObj let copyObj = shadowClone(sourceObj); // 修改 sourceObj copyObj.number = 2; copyObj.string = 'source2'; copyObj.boolean = false; copyObj.arr[0].name = 'arr2'; copyObj.func = () => 'sourceFunc2'; copyObj.obj.string = 'obj2'; copyObj.obj.func = () => 'objFunc2'; // 执行 console.log(sourceObj); /* { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr2'}], func: () => 'sourceFunc1', obj: { func: () => 'objFunc2', string: 'obj2' } } */
深拷贝可以实现真正意义上的对象的拷贝,实现方法就是递归调用“浅拷贝”。深拷贝会创造一个如出一辙的对象,其内容地址是自助分配的,拷贝结束以后,内存中的值是彻底相同的,可是内存地址是不同的,目标对象跟源对象不共享内存,修改任何一方的值,不会对另一方形成影响。
/* sourceObj 表示源对象 * 执行完函数,返回目标对象 */ function deepClone (sourceObj = {}) { let targetObj = Array.isArray(sourceObj) ? [] : {}; let copy; for (var key in sourceObj) { copy = sourceObj[key]; if (typeof(copy) === 'object') { if (copy instanceof Object) { targetObj[key] = deepClone(copy); } else { targetObj[key] = copy; } } else if (typeof(copy) === 'function') { targetObj[key] = eval(copy.toString()); } else { targetObj[key] = copy; } } return targetObj; }
// 定义 sourceObj let sourceObj = { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr1'}], func: () => 'sourceFunc1', obj: { string: 'obj1', func: () => 'objFunc1' } } // 拷贝sourceObj let copyObj = deepClone(sourceObj); // 修改 source copyObj.number = 2; copyObj.string = 'source2'; copyObj.boolean = false; copyObj.arr[0].name = 'arr2'; copyObj.func = () => 'sourceFunc2'; copyObj.obj.string = 'obj2'; copyObj.obj.func = () => 'objFunc2'; // 执行 console.log(sourceObj); /* { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr1'}], func: () => 'sourceFunc1', obj: { func: () => 'objFunc1', string: 'obj1' } } */
两个方法能够合并在一块儿:
/* deep 为 true 表示深复制,为 false 表示浅复制 * sourceObj 表示源对象 * 执行完函数,返回目标对象 */ function clone (deep = true, sourceObj = {}) { let targetObj = Array.isArray(sourceObj) ? [] : {}; let copy; for (var key in sourceObj) { copy = sourceObj[key]; if (deep && typeof(copy) === 'object') { if (copy instanceof Object) { targetObj[key] = clone(deep, copy); } else { targetObj[key] = copy; } } else if (deep && typeof(copy) === 'function') { targetObj[key] = eval(copy.toString()); } else { targetObj[key] = copy; } } return targetObj; }
(1)若拷贝数组是纯数据(不含对象),能够经过concat() 和 slice() 来实现深拷贝;
let a = [1, 2]; let b = [3, 4]; let copy = a.concat(b); a[1] = 5; b[1] = 6; console.log(copy); // [1, 2, 3, 4]
let a = [1, 2]; let copy = a.slice(); copy[0] = 3; console.log(a); // [1, 2]
(2)若拷贝数组中有对象,可使用 concat() 和 slice() 方法来实现数组的浅拷贝。
let a = [1, {name: 'hh1'}]; let b = [2, {name: 'kk1'}]; let copy = a.concat(b); copy[1].name = 'hh2'; copy[3].name = 'kk2'; console.log(copy); // [1, {name: 'hh2'}, 2, {name: 'kk2'}]
不管 a[1].name 或者 b[1].name 改变,copy[1].name 的值都会改变。
let a = [1, {name: 'hh1'}]; let copy = a.slice(); copy[1].name = 'hh2'; console.log(a); // [1, {name: 'hh2'}]
改变了 a[1].name 后,copy[1].name 的值也改变了。
Object.assign()、Object.create() 都是一层(根级)深拷贝,之下的级别为浅拷贝。
(1) 若拷贝对象只有一级,能够经过 Object.assign()、Object.create() 来实现对象的深拷贝;
let sourceObj = { str: 'hh1', number: 10 } let targetObj = Object.assign({}, sourceObj) targetObj.str = 'hh2' console.log(sourceObj); // {str: 'hh1', number: 10}
let sourceObj = { str: 'hh1', number: 10 } let targetObj = Object.create(sourceObj) targetObj.str = 'hh2' console.log(sourceObj); // {str: 'hh1', number: 10}
(2) 若拷贝对象有多级, Object.assign()、Object.create() 实现的是对象的浅拷贝。
let sourceObj = { str: 'hh', number: 10, obj: { str: 'kk1' } } let targetObj = Object.assign({}, sourceObj) targetObj.obj.str = 'kk2' console.log(sourceObj); // { // str: 'hh', // number: 10, // obj: { // str: 'kk2' // } // }
let sourceObj = { str: 'hh', number: 10, obj: { str: 'kk1' } } let targetObj = Object.create(sourceObj) targetObj.obj.str = 'kk2' console.log(sourceObj); // { // str: 'hh', // number: 10, // obj: { // str: 'kk2' // } // }
修改了 targetObj.obj.str 的值以后,sourceObj.obj.str 的值也改变了。
对象的解构同 Object.assign() 和 Object.create(),都是一层(根级)深拷贝,之下的级别为浅拷贝。
(1)若拷贝对象只有一层,能够经过对象的解构来实现深拷贝;
let sourceObj = { str: 'hh1', number: 10 } let targetObj = {...sourceObj}; targetObj.str = 'hh2' console.log(sourceObj); // {str: 'hh1', number: 10}
(2)若拷贝对象有多层,经过对象的解构实现的是对象的浅拷贝。
let sourceObj = { str: 'hh', number: 10, obj: { str: 'kk1' } } let targetObj = {...sourceObj}; targetObj.obj.str = 'kk2' console.log(sourceObj); // { // str: 'hh', // number: 10, // obj: { // str: 'kk2' // } // }
用 JSON.stringify() 把对象转成字符串,再用 JSON.parse() 把字符串转成新的对象,能够实现对象的深复制。
let source = ['hh', 1, [2, 3], {name: 'kk1'}]; let copy = JSON.parse(JSON.stringify(source)); copy[2][1] = 4; copy[3].name = 'kk2'; console.log(source); // ['hh', 1, [2, 3], {name: 'kk1'}]
能够看出,虽然改变了 copy[2].name 的值,可是 source[2].name 的值没有改变。
JSON.parse(JSON.stringify(obj)) 不只能复制数组还能够复制对象,可是几个弊端:
1)它会抛弃对象的 constructor,深拷贝以后,无论这个对象原来的构造函数是什么,在深拷贝以后都会变成 Object;
2)这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些可以被 json 直接表示的数据结构。RegExp 对象是没法经过这种方式深拷贝。
3)只有能够转成 JSON 格式的对象才能够这样用,像 function 没办法转成 JSON。
如下两种库都能实现深浅拷贝,有各自的使用方法。
具体使用能够参考:官方文档
具体使用能够参考:官方文档