不管是工作中还是面试过程中,我们都经常会遇到js深拷贝浅拷贝的问题,那么js为什么会有深拷贝和浅拷贝呢?为什么说它的本质是栈内存与堆内存的问题呢?

为了从根本上理解深拷贝浅拷贝,那么我们首先说说js的栈内存与堆内存,如果我么理解了栈内存与堆内存那么深拷贝与浅拷贝自然就不是问题了。

先上个deepClone函数: 
// params: 传参        function deepClone(params) {            // 判断一下对象是不是引用类型(数组和对象),null是特殊情况            if (typeof params === ‘object’&&params!=null) {                // 判断下 params 到底是数组还是对象,同时赋默认值                let cloneParams = (params instanceof Array) ? [] : {};                // 循环遍历 params                for (const key in params) {                    // 走递归函数                    cloneParams[key] = deepClone(params[key]);                }                // 返回                return cloneParams;            } else if (typeof params === ‘function’) {                // 如果是函数                return eval(‘(‘ + params.toString() + ‘)’)            } else {                // 如果是基本类型的值直接 return,这里null也是作为基本类型直接 return                return params;            }        };
        let obj = {            name: ‘js深拷贝’,            type: {                object: ‘对象’,                array: “数组”,                null:null,                fun:function () {                    console.log(this.name)                },                basicDataType:[‘string’,’number’,’undefined’,’boolean’],            },        }        console.log(deepClone(obj))

一: 数据类型

为了更好容易的理解堆和栈,首先来复习一下js中的数据类型。
在js中数据类型主要分为以下两大类:

  • 基本类型:String,Number,Boolean,Null,Undefined,这5种基本数据类型它们是直接按值存放的,所以可以直接访问。
  • 引用类型:Function,Array,Object,当我们需要访问这三种引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

现在咱再来瞅一瞅概念,什么是堆,什么是栈!

二: 什么是堆什么是栈

其实了解过一些数据结构的兄台应该都对堆和栈有一定的认识,堆栈不仅仅只有js中才有这个概念,不过这片文章只针对js来进行描述。

  • 栈(stack): 由操作系统自动分配内存空间,自动释放,存储的是基础变量以及一些对象的引用变量,占据固定大小的空间。
  • 堆(heap):由操作系统动态分配的内存,大小不定也不会自动释放,一般由程序员分配释放,也可由垃圾回收机制回收。

三: 堆和栈的区别

1、栈:基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的数组或者对象的地址,这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。
优点:相比于堆来说存取速度会快,并且栈内存中的数据是可以共享的,例如同时声明了var a = 1和var b = 1,会先处理a,然后在栈中查找有没有值为1的地址,如果没有就开辟一个值为1的地址,然后a指向这个地址,当处理b时,因为值为1的地址已经开辟好了,所以b也会同样指向同一个地址。
缺点:相比于堆来说的缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

2、堆:堆内存中的对象不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递)。创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
关于堆的优缺点大家只要看一下栈的优缺点相信就会有自己的判断了。

四: 传值与传址

基本类型与引用类型最大的区别实际就是传值与传址的区别。瞅一瞅这个例子:

var a = [1,2,3,4,5];var b = a;var c = a[0];alert(b);//[1,2,3,4,5] alert(c);//1 //改变数值 b[4] = 6;c = 7;alert(a[4]);//6alert(a[0]);//1

以上的例子中可以看出改变b的值时,a的数组也发生了改变;但是改变c的值时,a的数组毫无变化,这个就是传值和传址的区别。
因为a是数组,属于引用类型,所以它赋予给b的时候传的是栈中的地址(指向a这个数组在堆内存中的地址),而不是直接传的堆内存中的对象,当b发生改变时,也就会根据地址回到a在堆内存中的位置进行修改;而c仅仅是从a在堆内存中获取的一个数据值,并保存在栈中,所以c是直接在栈中修改,并且不能指向a在堆内存中的地址。

传值和传址简图

五: 深浅拷贝

1、浅拷贝:直接来例子,看完例子在讲解。

var a = { key1:"11111" }function Copy(p) {var c = {};for (var i in p) {   c[i] = p[i]; }return c;  } a.key2 = ['小辉','小辉'];var b = Copy(a);    b.key3 = '33333'; alert(b.key1); //11111 alert(b.key3); //33333 alert(a.key3); //undefinedb.key2.push("大辉");alert(b.key2); //小辉,小辉,大辉alert(a.key2); //小辉,小辉,大辉

从以上例子可以看出浅拷贝其实可以理解成只拷贝属性的第一层,在复制属性的基本类型时,直接用等号完成,如果遇到属性还是引用类型就会先将堆内存中的地址复制过去。


a对象中key1属性是字符串,key2属性是数组。a拷贝到b,key1和key2属性均顺利拷贝。给b对象新增一个字符串类型的属性key3时,b能正常修改,而a中无定义。说明子对象的key3(基本类型)并没有关联到父对象中,所以undefined。
但是,若修改的属性变为对象或数组时,那么父子对象之间就会发生关联,因为指向的堆内存中的地址是同一个。从以上弹出结果可知,我对b对象进行修改,a、b的key2属性值(数组)均发生了改变。其在内存的状态,可以用下图来表示。

浅拷贝

2、深拷贝:如果我们希望父子对象在任何时候都不产生关联,那我们就得用深度拷贝了,其实就是用递归的方法将引用类型的属性都赋值给子对象就可以了。瞅例子:

function Copy(p, c) {var c = c || {};for (var i in p) {if (typeof p[i] === 'object') {      c[i] = (p[i].constructor === Array) ? [] : {};      Copy(p[i], c[i]);   } else {      c[i] = p[i];   } }return c;  } a.key2 = ['小辉','小辉'];var b={}; b = Copy(a,b); b.key2.push("大辉"); alert(b.key2); //小辉,小辉,大辉 alert(a.key2); //小辉,小辉

由上可知,修改b的key2数组时,没有使a父对象中的key2数组新增一个值,即子对象没有影响到父对象a中的key2。简单的理解就是此时此刻,b中key2的数组和a中key2的数组虽然赋值相同,但是指向堆内存中的地址已经不是同一个了,所以不管修改哪一个key2都不会影响到另一个。其存储模式大致如下:

最后再总结一下:

由于object array的数据是存在堆内存,变量在栈内存的值实际是堆内存的指针,所以简单的=赋值后新的变量在栈内存中的值就是一个堆内存的指针,我们要实现深拷贝,就要在赋值的时候把堆内存的值递归至可以保存在栈内存的值,然后再赋值给新的对象,这样就实现了深拷贝,有点绕,不过理解了问题的本质后还是很好理解的,哈哈

本文参考:https://www.jianshu.com/p/3541b98c786e

微信交流(备注:前端)