JavaScript 中的浅拷贝和深拷贝

版权声明:转载请声明原地址 https://blog.csdn.net/dk2290/article/details/87774932

一.前言

每一个前端的 JavaScript 之路不一定是由《JavaScript 高级程序设计》开启的,但是每一位前端都一定被“按值传递”和“按引用传递”这两个概念坑过。现在你我应该都很清楚,在 JavaScript 中的object类型是按引用传递的,但是在函数参数中,所有参数都是按值传递的。

我们今天要谈的东西,就起源于object型数据的复制与再操作,简单来说,就是我们今天的主题:对象的浅拷贝和深拷贝。

二.按引用传递是什么含义?

首先,我们需要快速回想一下在 JavaScript 中基本数据类型有哪些,请看下面:

1. number
2. string
3. boolean
4. null
5. undefined
6. symbol

引用数据类型只有一种:

object

当然,由此衍生出来的变种也有很多,包括Array,function等等,也可以看成是对象的一种。

接着我们得复习下 JavaScript 的堆栈中是怎么存储数据的。在 JS 中内存的使用和分配与其他语言也大同小异:

堆是动态分配内存,内存大小不一,也不会自动释放。栈是自动分配相对固定大小的内存空间,并由系统自动释放。

对以上6种基本数据类型来说,

当我们在 JavaScript 中声明一个简单基础数据类型并初始化它的值时:

const NAME = 'waw';

此时这个值将以键值对的形式key:'NAME', value:'waw'保存在栈中。

但是对引用类型的数据来说却并不是如此,对它来说,虽然也是以键值对形式存储于栈中,但是value部分存储的是指向堆中的地址,如:key:'NAME', value: 指向堆的指针,而value真正的值则存储在堆中。而在 JavaScript 中是不允许直接操作堆中的内容的,所以我们日常操作对象时实际上操作的是对象的引用。

到这儿为止,咱们的提前知识储备已经差不多了,从上面的内容我们可以达成一个基础的共识,那就是基础数据类型是没有浅拷贝深拷贝之说的,大家都是存储在栈中的社会主义公民,都很平等。所以为了接下来讲解的层次性,我将从数组和对象,基本数据类型和引用数据类型,两种角度出发来给大家讲讲浅拷贝和深拷贝。

三.JavaScript 的浅拷贝

3.1 从基本数据类型的数组角度来说

形如var arr = [1, '1', true, null, undefined, Symbol()],这一类数组内元素的集合,我们都可以称它们为基本数据类型的数组。

浅拷贝最简单的就是var a = b了。举个例子:

var arr = [1, '1', true, null, undefined, Symbol()];
var arrCopy = arr;
arrCopy[0] = 2;
console.log(arr)   
console.log(arrCopy)

结果如下:
在这里插入图片描述

很明显,这就是个最简单的浅拷贝。当然我们也可以自己实现一个基础的浅拷贝函数:

function shallowClone(obj){
    let cloneObj = {}; 
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            cloneObj[key] = obj[key];
        }
    }
    return cloneObj;
}

3.2 从引用数据类型的数组角度来说

什么是引用数据类型的数组?举个简单例子,形如[[1, 2], {age: 1}, Number(1), String(2)]的数组内元素的集合,都可以称为引用数据类型。当然,我们平常在业务中最常见的就是多维数组或者是对象数组。

对它来说,浅拷贝最直接的方式也是var a = b;举个例子:

var arr = [[1, 2], {age: 1}, Number(1), String(2)];
var arrCopy = arr;
arrCopy[0] = [3, 4];
console.log(arr)   
console.log(arrCopy)

结果如下:
在这里插入图片描述

3.3 从对象角度来说

从对象角度来说,它就不像数组那样好区分基本数据类型和引用数据类型了,因为毕竟是key=> value键值对形式来存储的结构。

我们一般也可以通过var a=b来进行浅拷贝。举个例子:

var arr = {name:'waw', age:1};
var arrCopy = arr;
arrCopy.name = 'gcc';
console.log(arr)   
console.log(arrCopy)

结果如下:
在这里插入图片描述

这儿有个很有意思的事,ES6有一个Object.assgin()方法,可以拷贝(合并)对象并返回新对象。我们试试用它来复制上面的对象看会输出什么:

var arr = {name:'waw', age:1};
var arrCopy = Object.assign({}, arr);
arrCopy.name = 'gcc';
console.log(arr)   
console.log(arrCopy)

结果如下:
在这里插入图片描述

emmm…从结果来看,像是一个深拷贝方法,不着急,我们试试稍微复杂的结构:

var arr = {name:'waw', age:1, love:{ ball: 'football', game: 'tecent'}};
var arrCopy = Object.assign({}, arr);
arrCopy.love.game= 'alibaba';
console.log(arr);   
console.log(arrCopy);

结果如下:
在这里插入图片描述

你还可以试试其他层次大于一级的对象结构,我们会发现Object.assign()这个方法在大于一级的对象结构下进行的都是浅拷贝。所以,我们一般是建议在业务里如果想使用深拷贝来处理对象,避免使用Object.assign()函数。

四.JavaScript 的深拷贝

要谈深拷贝,首先我们要有一个共识,就是何为深拷贝?简单来说,就是对某个对象会不断递归去遍历复杂对象中的每一个层级,最后输出的这样一个过程,可以称之为深拷贝。有了这个前提,我们再来看看深拷贝在数组和对象中的具体实现。

4.1 从基本数据类型的数组角度来说

对于数组的深拷贝来说,其实你可以从这个角度出发:

在 JavaScript 中所有操作数组的方法里,如果此操作是不修改原数组而是返回新数组,那么可以判定这个方法可以做到深拷贝。

接下来我用几个例子来支撑上面这个理论:

var arr = ['a', 'b', 'c'];
var arrCopySlice = arr.slice(0);
var arrCopyConcat = [].concat(arr);
var arrCopyMap = arr.map(el => el);
var arrCopyFilter = arr.filter(el => el);
var arrCopy = [...arr];
arrCopySlice[0] = 'test';
arrCopyConcat[0] = 'test';
arrCopyMap[0] = 'test';
arrCopyFilter[0] = 'test';
arrCopy[0] = 'test';
console.log('arr', arr)
console.log('arrCopySlice', arrCopySlice)
console.log('arrCopyConcat', arrCopyConcat)
console.log('arrCopyMap', arrCopyMap)
console.log('arrCopyFilter', arrCopyFilter)
console.log('arrCopy', arrCopy)

可以看到,我使用了arr.slice(), arr.concat(), arr.map(), arr.filter(), [...arr]这五个方法,以上5个方法都是满足在不修改原数组的前提下返回新数组这一原则。输出结果如下:
在这里插入图片描述

从输出可以看出,改变了arrCopy并不会影响原数组arr。当然,我们还可以换句话来理解:

所谓数组深拷贝,也可以理解为无论此数组内的数据有多少层级,它都可以一层层遍历去获取。

而以上5个例子,我们要注意都是基本数据类型,这意味着大家都是一层的结构,更深的层次,以上5个方法,都做不到深拷贝了,要切记这一点。

4.2 从引用数据类型的数组角度来说

这儿也有个很有意思的事,我们对数组使用Object.assgin()方法时,也是可以拷贝数组的,但是结果与拷贝对象有所不同:

var arr = [1, '1', {a:11}];
var arrCopy = Object.assign([],arr);
arrCopy[2].a = 22;
console.log(arr)   
console.log(arrCopy)

结果如下:
在这里插入图片描述
结果与对对象进行 Object.assgin() 操作时不同,为浅拷贝,所以对于数组来说,一级结构也不能使用 Object.assgin() 来进行深拷贝。

对于更深层的数组或是对象(二级结构或以上)来说,我们一般会采取两种思路:

  1. 序列化和序列化。(即 JSON.parse()JSON.stringify())
  2. 深递归

所以这两种思路就放在对象的深拷贝里一起讲了。

4.3 从对象角度来说

主要说说两种思路。

第一种,序列化和反序列化

const obj = {a:1, b:'str', c:{name:'waw', age:20}};
let objCopy = JSON.parse(JSON.stringify(obj));
objCopy.c.age = 18;
console.log(obj)   
console.log(objCopy)

输出结果如下:
在这里插入图片描述
就平常的业务情况来说,肯定是够用的,当然它也不是银弹,它只能处理对象和数组内容,除此之外的 regdateerr等类型对象都无法处理。

第二种. for in 深递归

function isObject(obj){
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}

function deepClone(obj){
     let isArray = Array.isArray(obj); 
     var cloneObj = isArray ? [] : {};
     for(let key in obj){
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
     }
     return cloneObj;
}

let obj = {a:1, b:2, c:{name:'waw', age:10}};
let copyObj = deepClone(obj);
copyObj.c.age = 20;
console.log('obj', obj)
console.log('copyObj', copyObj)

输出结果如下:
在这里插入图片描述

五.深拷贝的特殊情况

5.1 DateRegExp 对象的深拷贝失效

在 MDN 的结构化克隆算法中有这么一段话:
在这里插入图片描述

对应到我们现在的问题来说,意味着error, Date, RegExp 以及 Function 对象不能被上面的所深克隆。

举个例子:

let obj = {date:new Date(), reg:/reg/g, func:function(){}};
let copyObj = deepClone(obj);
console.log('obj', obj)
console.log('copyObj', copyObj)

我们可以看到结果:
在这里插入图片描述
copyObj里的属性全是空对象{},深拷贝失败。

怎么解决呢?可以通过构建它们对应的构造函数来处理这个问题:

function isObject(obj){
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}

function deepClone(obj){
     let cloneObj;
     let Constructor = obj.constructor;
     switch(Constructor){
        case RegExp:
            cloneObj = new Constructor(obj);
            break;
        case Date:
            cloneObj = new Constructor(Number(obj));
            break;
        default:
            cloneObj = new Constructor();
     }
     for(let key in obj){
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
     }
     return cloneObj;
}
let obj = {date:new Date(), reg:/reg/g};
let copyObj = deepClone(obj);
console.log('obj', obj)
console.log('copyObj', copyObj)
console.log(obj.date === copyObj.date)
console.log(obj.reg === copyObj.reg)

输出如下:
在这里插入图片描述

5.2 环对象

什么是环对象?形如下面这种结构就叫环对象:

const obj = {};
obj.private = obj;

我们用上面的那个 deepClone() 函数对此对象进行复制,则会抛出栈溢出的异常:Maximum call stack size exceeded

怎么处理呢?其实原理也简单,就是用一个哈希表来存储已经拷贝过的内容,然后在拷贝前对哈希表里的内容进行判断,如果对象已经存在,则直接返回此对象。

function isObject(obj){
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}


function deepClone(obj, hash = new WeakMap()){
     let cloneObj;
     let Constructor = obj.constructor;
     if(hash.has(obj)){
        return hash.get(obj);
     }
     
     switch(Constructor){
        case RegExp:
            cloneObj = new Constructor(obj);
            break;
        case Date:
            cloneObj = new Constructor(Number(obj));
            break;
        default:
            cloneObj = new Constructor();
            hash.set(obj, cloneObj);
     }
     
     for(let key in obj){
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key]
     }
     return cloneObj;
}
let obj = {date:new Date(), reg:/reg/g};
obj.private = obj;
let copyObj = deepClone(obj);
console.log('obj', obj)
console.log('copyObj', copyObj)

输出结果如下:
在这里插入图片描述
我们可以看到,private 中是一个 Circular 对象,即无限自循环对象。拷贝是成功的。

你也可以把上面的代码放到浏览器中执行,就可以看到 Circular 对象的具体内容:
在这里插入图片描述

5.3 原型链上的属性

我们知道,函数有protorype属性,被称为原型链。对象也有原型链,其属性名为__proto__。如果你想拷贝某个对象__proto__上的属性,你可以使用for...in。看个例子:

const obj = {
	name:'waw',
	age:11
};
const obj2 = Object.create(obj);
for(let key in obj2){
	console.log(key)
}

输出结果如下:
在这里插入图片描述

我们可以看到,obj2 本是一个空对象,并没有属性,但是 for...in 却打印出了 nameage 属性,从上图也可以看出,这两个属性实际上是原型链 __proto__ 上的属性。

到这里深拷贝的内容差不多就结束了,其实还有很多可以继续挖掘的地方,比如说:Symbol 类型的深拷贝, Symbol 类型作为 key 时的深拷贝,不可枚举属性的深拷贝,function 类型的深拷贝等等,由于篇幅原因就不在此篇文章中细说了,忙完手里的事我会再接着写一篇后续来补充更深入的内容。

猜你喜欢

转载自blog.csdn.net/dk2290/article/details/87774932