深浅拷贝在我们平常开发中也会经常用到,下来我们来具体谈谈深浅拷贝。
其实深拷贝和浅拷贝的主要区别就在于其内存中的存储类型不同。堆和栈都是内存中划分出来用来存储的区域。
在深浅拷贝学习之前,先看下ECMAScript中的数据类型。主要分为基本数据类型和引用数据类型。
存放在栈中的数据大小确定,内存空间大小可以分配,是直接按值存放的,可直接访问。
基本数据类型值不可变
javascript中的基本数据类型(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。
基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值也是不会改变的,如:
var str = "abc";
console.log(str[1]="l"); // l
console.log(str); // abc
基本类型的比较是值的比较
基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的,如:
var a = 1;
var b = 1;
console.log(a === b);//true
比较的时候最好使用严格等,因为 == 是会进行类型转换的,比如:
var a = 1;
var b = true;
console.log(a == b);//true
var p1 = {name:'qiao'};
var p2 = {name:'zhang'};
var p3 = {name:'wang'};
引用类型值可变
引用类型是可以直接改变其本身的值的。
var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5
console.log(a); // [1,5,3]
引用类型的比较是引用的比较
所以每次我们对js中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。如:
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
虽然变量a和变量b都是表示 1,2,3的数组,但是其在内存中的位置不一样,即变量a和变量b指向的不是同一个对象,所以他们是不相等的。
传值与传址
通过上述了解基本数据类型与引用类型的区别之后,我们明白传值与传址的区别了。
在我们进行赋值操作的时候,基本数据类型的赋值是在内存中新开辟一段栈内存,然后再将值赋值到新的栈中。例如:
var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10
即基本类型的赋值的两个变量是两个独立相互不影响的变量。
引用类型的赋值是传址。即只是改变指针的指向,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。如:
var a = {};
var b = a;
a.name = 'qiao';
console.log(a.name); // 'qiao'
console.log(b.name); // 'qiao'
b.age = 26;
console.log(b.age);// 26
console.log(a.age);// 26
console.log(a == b);// true
我们知道javascript中一般有按值传递和按引用传递两种复制方式:
Number,String,Boolean,Null,Undefined,Symbol,一般存放于内存中的栈区,存取速度快,存放量小;
Object,Array,Function,一般存放与内存中的堆区,存取速度慢,存放量大,其引用指针存于栈区,并指向引用本身。
我们经常说的深浅拷贝是针对引用类型来说的:
根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用, 深拷贝在计算机中开辟了一块内存地址用于存放复制的对象,而浅拷贝仅仅是指向被拷贝的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。
var arr = [1, 2, 3];
var newArr = arr;
newArr[0] = "11";
console.log(arr); // ["11", 2, 3]
console.log(newArr); // ["11", 2, 3]
console.log(arr==newArr); // true
console.log(arr===newArr); // true
var obj = { a: {a: "js"}, b: 22 };
var newObj = Object.assign({}, obj);
newObj.a.a = "hello js";
console.log(obj); // { a: {a: "hello js"}, b: 22 };
console.log(newObj); // { a: {a: "hello js"}, b: 22 };
console.log(obj.a.a==newObj.a.a); // true
console.log(obj.a.a===newObj.a.a); // true
var obj = { a: {a: "js"}, b: 22 };
var newObj = $.extend({}, obj);
newObj.a.a = "hello js";
console.log(obj); // { a: {a: "hello js"}, b: 22 };
console.log(newObj); // { a: {a: "hello js"}, b: 22 };
console.log(obj.a.a==newObj.a.a); // true
console.log(obj.a.a===newObj.a.a); // true
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 obj = { a: 1, b: 2};
var newObj = { a: obj.a, b: obj.b};
newObj.b = 111;
console.log(obj); // { a: 1, b: 2 }
console.log(newObj); // { a: 1, b: 111 }
console.log(obj == newObj); // false
console.log(obj === newObj); // false
var obj = { a: {a: "js"}, b: 22 };
var newObj = Object.assign({}, obj);
newObj.b = 222;
newObj.a.a = 'javascript';
console.log(obj); // { a: { a: 'javascript' }, b: 22 }
console.log(newObj); // { a: { a: 'javascript' }, b: 222 }
console.log(obj==newObj); // false
console.log(obj===newObj); // false
复杂深拷贝常用的方法如下(二维的数据结构及以上):
var obj = { a: {a: "js"}, b: 33 };
var newObj = JSON.parse(JSON.stringify(obj));
newObj.b = 333;
newObj.a.a = "hello js";
console.log(obj); // { a: { a: 'js' }, b: 33 }
console.log(newObj); // { a: { a: 'hello js' }, b: 333 }
console.log(obj==newObj); // false
console.log(obj===newObj); // false
var obj = { a: {a: "js"}, b: 22 };
var newObj = $.extend(true, {}, obj);
newObj.a.a = "hello js";
console.log(obj); // { a: "js", b: 22 };
console.log(newObj); // { a: "hello js", b: 22 };
console.log(obj==newObj); // false
console.log(obj===newObj); // false
var obj = { a: {a: "js"}, b: 22 };
var newObj = _.cloneDeep(obj);
newObj.a.a = "hello js";
console.log(obj); // { a: "js", b: 22 };
console.log(newObj); // { a: "hello js", b: 22 };
console.log(obj==newObj); // false
console.log(obj===newObj); // false
function deepCopyTwo(obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj == 'object') {
for (const key in obj) {
//判断obj子元素是否为对象,如果是,递归复制
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepCopyTwo(obj[key]);
} else {
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
return objClone;
}
let obj = {a: 11, b: function(){}, c: {d: 22}};
let f = deepCopyTwo(obj);
console.log(f); // { a: 11, b: [Function: b], c: { d: 22 } }
深拷贝最常用的就是上述这些方法,当然还有其他的一些库,比如deepCopy等,数组常用contact和slice来实现深拷贝,不同的方法有其最好的适用环境。
下面我们用数据具体分析一下一维数据结构和二维数据结构在不同方法下的性能对比。
深拷贝一维数据结构方法对比:
var obj = [];
for (var i = 0; i < 100; i++) {
obj[i] = Math.random();
}
console.time("assign");
var newObj = Object.assign({}, obj);
console.timeEnd("assign");
console.time("JSON.parse(JSON.stringify())");
var newObj = JSON.parse(JSON.stringify(obj));
console.timeEnd("JSON.parse(JSON.stringify())");
console.time("$.extend");
var newObj = $.extend(true, {}, obj);
console.timeEnd("$.extend");
console.time("Loadsh.cloneDeep");
var newObj = _.cloneDeep(obj);
console.timeEnd("Loadsh.cloneDeep");
经过上述分析发现,一维数据结构的深拷贝方法性能最佳的为Object.assign();
深拷贝二维数据结构用时对比:
var obj = [];
for (var i = 0; i < 100; i++) {
obj[i] = {};
for (var j = 0; j < 100; j++) {
obj[i][j] = Math.random();
}
}
console.time("JSON.parse(JSON.stringify())");
var newObj = JSON.parse(JSON.stringify(obj));
console.timeEnd("JSON.parse(JSON.stringify())");
console.time("$.extend");
var newObj = $.extend(true, {}, obj);
console.timeEnd("$.extend");
console.time("Loadsh.cloneDeep");
var newObj = _.cloneDeep(obj);
console.timeEnd("Loadsh.cloneDeep");
经过上述分析发现,二维数据结构的深拷贝方法性能最佳的为JSON.parse(JSON.stringify());
如果是数组,可以用数组的一些方法如:slice、concat 返回一个新数组的特性来实现浅拷贝。
var arr = ['old', 1, true, null, undefined];
var newArr = arr.concat();
newArr[0] = 'new';
console.log(arr) // ["old", 1, true, null, undefined]
console.log(newArr) // ["new", 1, true, null, undefined]
var arr = ['old', 1, true, null, undefined];
var newArr = arr.slice();
newArr[0] = 'new';
console.log(arr) // ["old", 1, true, null, undefined]
console.log(newArr) // ["new", 1, true, null, undefined]
但是如果数组嵌套了对象或者数组的话,比如:
var arr = [{old: 'js'}, ['js']];
var new_arr = arr.concat();
arr[0].old = 'jss';
arr[1][0] = 'jss';
console.log(arr) // [ { old: 'jss' }, [ 'jss' ] ]
console.log(new_arr) // [ { old: 'jss' }, [ 'jss' ] ]
从上述代码中发现,新老数组都发生了变化,即如果数组元素是基本类型,拷贝一份,互不影响,而如果是对象或数组,就会只拷贝对象和数组的引用,即无论在新旧数组进行修改,两者都会发生变化。我们把这种复制引用的拷贝方法称之为浅拷贝,与之对应的就是深拷贝,即concat和slice只能实现一层数据的拷贝,即浅拷贝。
如何深拷贝一个数组?不仅适用于数组还适用于对象!
var arr = ['js', 1, true, ['js1', 'js2'], {js: 1}]
var newArr = JSON.parse( JSON.stringify(arr) );
console.log(newArr); // [ 'js', 1, true, [ 'js1', 'js2' ], { js: 1 } ]
和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 | |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
赋值 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |