ES6 深拷贝_Javascript 经典面试之深拷贝VS浅拷贝

不点蓝字,我们哪来故事?

这是一道经典的面试题,相信大多数同学都有被面试官问过的经历,那么你能实现几种深拷贝和浅拷贝的方法呢?是不是又问到了你的知识盲区,那让我们来一起总结常用的深浅拷贝(克隆)的方法吧!

开始之前

9ade164b7b0150e73876a70f7ffc1be8.png

在开始之前,我们要先明确一下 JS 的数据类型,以及数据存储(栈和堆)的概念:

JS 数据类型分为基本数据类型引用数据类型(引用数据类型又称复杂数据类型)

基本数据类型引用数据类型
NumberObject
StringFunction
BooleanArray
UndefindDate
NullRegExp
Symbol(ES6 新增)Math
BigInt(ES10 新增)...都是Object类型的实例对象
  • 基本数据类型和引用数据类型的储存方式区别:

基本数据类型:变量名和值都储存在栈内存中;

引用数据类型:变量名储存在栈内存中,值储存在堆内存中,堆内存中会提供一个引用地址指向堆内存中的值,而这个引用地址是储存在栈内存中的。

例如:

let obj = {    a: 100,    b: 'name',    c: [10,20,30],    d: {        x:10,    },}

obj 在内存中的储存如下:

栈内存栈内存堆内存
namevalval
a100---
b'name'---
cAAAFFF000(一个引用地址,指向堆内存的值)[10,20,30]
dBBBFFF000(一个引用地址,指向堆内存的值){ x:10 }

对这几个概念有了初步了解之后,接下来正式开始讲深浅拷贝。

浅拷贝

9ade164b7b0150e73876a70f7ffc1be8.png

何为浅拷贝?当 obj2 拷贝了 obj 的数据,且当 obj2 的改变会导致 obj 的改变时,此时叫 obj2 浅拷贝了 obj。

举个例子🌰:

let obj = {    a: '100',}let obj2 = obj;obj2.a = '200';console.log(obj.a)    // '200'

obj 直接赋值给 obj2 后,obj2 中 a 属性的改变导致了 obj 中 a 属性也发生了变化。

其实这里的原因也很简单,因为这种赋值方式只是将 obj 的堆内存地址赋值给了 obj2,obj 和 obj2 指向的是一个存储地址,是同一个内容,因此 obj2 的改变当然会引起 obj 的改变。

常见的浅拷贝

9ade164b7b0150e73876a70f7ffc1be8.png

我们以下面的对象为例:

let obj = {    a: '100',    b: undefined,    c: null,    d: Symbol(2),    e: /^\d+$/,    f: new Date,    g: true,    arr:[10,20,30],    school:{        name:'cherry'    },    fn: function fn() {        console.log('fn');    }

方法一:直接赋值

9ade164b7b0150e73876a70f7ffc1be8.png

直接赋值的方法就是我们刚才所举的例子🌰,这种方式实现的就是纯粹的浅拷贝,obj2 的任何变化都会反映在 obj 上。

方法二:使用对象的解构

9ade164b7b0150e73876a70f7ffc1be8.png
let obj2 = { ...obj }

方法三:使用循环

9ade164b7b0150e73876a70f7ffc1be8.png

对象循环我们使用 for in 循环,但 for in 循环会遍历到对象的继承属性,我们只需要它的私有属性,所以可以加一个判断方法:hasOwnProperty 保留对象私有属性。

let obj2 = {};for(let i in obj) {    if(!obj.hasOwnProperty(i)) break; // 这里使用 continue 也可以    obj2[i] = obj[i];}

方法四:Object.assign(target,source)

9ade164b7b0150e73876a70f7ffc1be8.png

这是ES6中新增的对象方法,对它不了解的见ES6对象新增方法。

let obj2 = {};Object.assign(obj2,obj); //将 obj 拷贝到 obj2

浅拷贝总结:

9ade164b7b0150e73876a70f7ffc1be8.png

方法一就是纯粹的浅拷贝,obj2 的任何变化都会反映在 obj 上。方法二、三、四都可以实现第一层的“深拷贝”,但无法实现多层的深拷贝。比如我们修改下 obj2 的值:

obj2.a = '200';console.log(obj.a);  // '100'// obj.a 属性未发生变化obj2.school.name = 'susan';console.log(obj.school.name);  // 'sucan'// obj.school.name 属性随着 obj2 而变化了

深拷贝

9ade164b7b0150e73876a70f7ffc1be8.png

这几种拷贝方法无法满足更深层级的拷贝,所以我们需要另一种万全之策--深拷贝

方法一:JSON.parse()和JSON.stringify

9ade164b7b0150e73876a70f7ffc1be8.png
let obj2 = JSON.parse(JSON.stringify(obj));obj2.schoole.name= 'susan';console.log(obj.school.name); // 'cherry'//obj 中属性值并没有改变,说明是深拷贝

这种方法是比较简单的深拷贝,在对象属性的类型比较简单的时候,我们可以采取这种方法快速深拷贝。

但当对象属性的类型较为复杂时,就会发现这种方法虽然能实现深拷贝,但也有很多坑,运行上面的代码后发现:

d8fa68f53d40622bfe531d9102aaa877.png
  • 值为 undefined 的属性在转换后丢失;

  • 值为 Symbol 类型的属性在转换后丢失;

  • 值为 RegExp 对象的属性在转换后变成了空对象;

  • 值为 函数对象的属性在转换后丢失;

  • 值为 Date 对象的属性在转换后变成了字符串;

  • 会抛弃对象的 constructor,所有的构造函数会指向Object;

  • 对象的循环引用会抛出错误。

最后两种坑,我们来简单测试下:

  • 会抛弃对象的 constructor,所有的构造函数会指向 Object

// 构造函数function person(name) {    this.name = name;}const Cherry = new person('Cherry');const obj = {    a: Cherry,}const obj2 = JSON.parse(JSON.stringify(obj));console.log(obj.a.constructor, obj2.a.constructor); // [Function: person] [Function: Object]
  • 对象的循环引用会抛出错误

const obj = {};obj.a = obj;const obj2 = JSON.parse(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON

是不是觉得坑很多?所以小伙伴们在使用这种方式深拷贝的时候,还是要多多注意下。

出现这种问题的原因和 JSON.stringify 方法的序列化规则有关系,关于JSON.stringify序列化的具体规则见 JSON.stringify 指南。

关于如何去 JSON.stringify 序列化也是一个比较有意思的问题,大家可以学习一下,毕竟面试官总是喜欢问到你不会为止。。。

方法二:手写 deepClone

9ade164b7b0150e73876a70f7ffc1be8.png

既然第一种方法有它的弊端,那最终极的方法,就是手写一个 deepClone 了。

用过lodash的小伙伴都知道lodash提供了_.cloneDeep 方法深克隆,它的源码里实现的比较复杂,考虑的情况比较多,我们写一个简单版的深拷贝可以在自己项目中使用即可。

d8fa68f53d40622bfe531d9102aaa877.png

简单的实现思路:

1.遍历带拷贝的对象,判断是不是原始值,若是,使用浅拷贝的方式进行赋值。

2.若是引用值,将特殊类型逐一进行过滤,并且兼容引用值是数组的情况。

3.待拷贝的对象里面的若是原始值,则浅拷贝即可实现,若还有引用值,则还需要重复进行上述一系列的判断(递归赋值)

上述思路用代码如何实现呢?

let obj = {    a: '100',    b: undefined,    c: null,    d: Symbol(2),    e: /^\d+$/,    f: new Date,    g: true,    arr: [10,20,30],    school:{        name: 'cherry',    },    fn: function fn() {        console.log('fn');    }}function deepClone(obj) {    // 先把特殊情况全部过滤掉 null undefined date reg    if (obj == null) return obj;  // null 和 undefined 都不用处理    if (obj instanceof Date) return new Date(obj);    if (obj instanceof RegExp) return new RegExp(obj);    if (typeof obj !== 'object') return obj;  // 普通常量直接返回    // 不直接创建空对象的目的:克隆的结果和之前保持相同的所属类,    // 同时也兼容了数组的情况    let newObj = new obj.constructor;    for (const key in obj) {        if (obj.hasOwnProperty(key)) {  // 不拷贝原型链上的属性            newObj[key] = deepClone(obj[key]);  // 递归赋值        }    }    return newObj;}let obj2 = deepClone(obj);console.log(obj2);

执行代码,得到 obj2 的结果和 obj 一致,且属性值的改变彼此互不影响。

Q:为什么 type null 会返回 object ?

A:因为在 js 的设计中,object的前三位标志是000,而 null 在32位表示中也全是0,因此,typeof null 也会打印出object

代码写到这里,我们就实现了一种比较简单的深拷贝,面试的时候如果你能写出上面的实现方法,应该算是及格啦!

但是,面对复杂的,多类型的对象,以上方法还是有诸多缺陷的。

比如我们为 obj 中的 school 对象添加一个 Symbol 类型的属性:

//==新增代码==let s1 = Symbol('s1');let obj = {    a: '100',    b: undefined,    c: null,    d: Symbol(2),    e: /^\d+$/,    f: new Date,    g: true,    arr: [10,20,30],    school:{        name: 'cherry',        //==新增代码==        [s1]: 's1'    },    fn: function fn() {        console.log('fn');    }}let obj2 = deepClone(obj);console.log(obj2);

执行代码后发现 school 中的 Symbol(s1): 's1'并没有拷贝成功。这是因为声明对象的 key 为 symbol 类型是不可枚举的,要解决这个问题,我们可以使用 Object 提供的 getOwnPrepertySymbols()方法来枚举对象中所有 key 是 symbol 类型的属性,这个属性的详细使用说明参见MDN,或者用 Reflect.ownKeys() 也可以实现。

还比如:如果在我们拷贝的对象被循环引用,deepClone就会一直执行下去导致爆栈,举个例子:

let obj = {    a: '100',    b: undefined,    c: null,    d: Symbol(2),    e: /^\d+$/,    f: new Date,    g: true,    arr: [10,20,30],    school:{        name: 'cherry',    },    fn: function fn() {        console.log('fn');    }}obj.h = obj;let obj2 = deepClone(obj);console.log(obj2);

执行上述代码后,控制台抛出栈溢出错误:Maximum call stack size exceeded其实解决循环引用的思路,就是在赋值之前判断当前值是否已经存在,避免循环引用,这里我们可以使用 es6 的 WeakMap 来生成一个 hash 表。

针对以上这两个问题,我们来优化一下代码:

let s1 = Symbol('s1');let obj = {    a: '100',    b: undefined,    c: null,    d: Symbol(2),    e: /^\d+$/,    f: new Date,    g: true,    arr: [10,20,30],    school:{        name:'cherry',        [s1]: 's1'    },    fn: function fn() {        console.log('fn');    }}obj.h = obj;function deepClone(obj, hash = new WeakMap()) {    //先把特殊情况全部过滤掉 null undefined date reg    if (obj == null) return obj;  //null 和 undefined 都不用处理    if (obj instanceof Date) return new Date(obj);    if (obj instanceof RegExp) return new RegExp(obj);    if (typeof obj !== 'object') return obj;  // 普通常量直接返回        //  防止对象中的循环引用爆栈,把拷贝过的对象直接返还即可    if (hash.has(obj)) return hash.get(obj);    // 不直接创建空对象的目的:克隆的结果和之前保持相同的所属类    // 同时也兼容了数组的情况    let newObj = new obj.constructor;    hash.set(obj, newObj)  // 制作一个映射表        //判断是否有 key 为 symbol 的属性    let symKeys = Object.getOwnPropertySymbols(obj);    if (symKeys.length) {        symKeys.forEach(symKey => {            newObj[symKey] = deepClone(obj[symKey], hash);        });    }    for (const key in obj) {        if (obj.hasOwnProperty(key)) {  // 不拷贝原型链上的属性            newObj[key] = deepClone(obj[key], hash);  // 递归赋值        }    }    return newObj;}let obj2 = deepClone(obj);console.log(obj2);

这样,一个比较完善的深拷贝就实现啦~

不过,完善但并不是完美,还有更高维度的问题需要优化。

比如:1.没有考虑 es6 中 Map 和 Set 的拷贝,2.递归消耗大量的内存会导致的爆栈等等等等,想要实现一个完美的深拷贝,还是有很多内容需要我们深度学习~

如果你还对深拷贝有兴趣或者想研究,可以阅读lodash 深拷贝相关代码,相信你会对深拷贝有进一步的理解~

写在最后

9ade164b7b0150e73876a70f7ffc1be8.png

最后,祝大家端午安康嗷,别忘了吃粽子~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值