浅谈javascript深拷贝和浅拷贝

深浅拷贝在我们平常开发中也会经常用到,下来我们来具体谈谈深浅拷贝。

堆和栈的区别

其实深拷贝和浅拷贝的主要区别就在于其内存中的存储类型不同。堆和栈都是内存中划分出来用来存储的区域。

  • 栈:是自动分配的内存空间,它由系统自动释放;
  • 堆:是动态分配的内存,大小不定也不会自动释放;

在深浅拷贝学习之前,先看下ECMAScript中的数据类型。主要分为基本数据类型和引用数据类型。

  • 基本数据类型(存放在栈中):undefined,boolean,number,string,null

存放在栈中的数据大小确定,内存空间大小可以分配,是直接按值存放的,可直接访问。

基本数据类型值不可变

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,一般存放与内存中的堆区,存取速度慢,存放量大,其引用指针存于栈区,并指向引用本身。

我们经常说的深浅拷贝是针对引用类型来说的:

  • 浅拷贝:指两个js 对象指向同一个内存地址,其中一个改变会影响另一个;
  • 深拷贝:指复制后的新对象重新指向一个新的内存地址,两个对象改变互不影响。

深浅拷贝的区别

根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用, 深拷贝在计算机中开辟了一块内存地址用于存放复制的对象,而浅拷贝仅仅是指向被拷贝的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。

浅拷贝

浅拷贝常用的方法如下:
  1. 简单的直接赋值操作
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
  1. Object.assign()方法是ES6的新函数,可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。拷贝的是对象的属性的引用,而不是对象本身,但是也可以实现一层深拷贝
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
  1. $.extend({},obj)使用递归思路实现了浅拷贝和深拷贝,第一个参数类型为Boolean,当为false的时候必须省略不写则是浅拷贝,当为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
  1. 自己动手实现一个浅拷贝
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;
}

深拷贝

简单深拷贝常用的方法如下(一维的数据结构):

  1. 手动的赋值操作:
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
  1. Object.assign()方法是ES6的新函数,只能简单的复制一层属性到目标对象,还得考虑兼容性
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

复杂深拷贝常用的方法如下(二维的数据结构及以上):

  1. JSON.parse(JSON.stringify(obj))是最简单粗暴的深拷贝,能够处理JSON格式的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝,而且会直接丢失相应的值,还有就是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理
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
  1. $.extend(true,{},obj)使用递归思路可以实现深拷贝,要求第一个参数必须为true:
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
  1. lodash中的_.clone(obj, true)等价于_.cloneDeep(obj) 两个方法,lodash花了大量的代码来实现ES6引入的大量新的标准对象,并针对存在环的对象的处理也是非常出色的,因此对于深拷贝来说lodash和其他库相比最友好:
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
  1. 自己实现一个简单的深拷贝deepCopyTwo(),即用递归去复制所有层级属性:
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 返回一个新数组的特性来实现浅拷贝。

  1. 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]
  1. slice实现浅拷贝
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只能实现一层数据的拷贝,即浅拷贝

数组的深拷贝

如何深拷贝一个数组?不仅适用于数组还适用于对象!

  1. JSON.parse( JSON.stringify(params)
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 } ]

总结:

  • 一维数据结构的深拷贝方法建议使用:Object.assign();
  • 二维数据结构及以上的深拷贝方法建议使用:JSON.parse(JSON.stringify());
  • 特别复杂的数据结构的深拷贝方法建议使用:Loadsh.cloneDeep();
  • 用一张表说明下赋值、深拷贝、浅拷贝的区别
和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
赋值 改变不会使原数据一同改变 改变不会使原数据一同改变
  • 只有在对象里有嵌套对象的情况下,才会根据需求讨论是要进行深拷贝还是浅拷贝
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章