乐趣区

关于javascript:Javascript-经典面试之深拷贝VS浅拷贝


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

开始之前

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

  • JS 数据类型分为 根本数据类型 援用数据类型(援用数据类型又称简单数据类型)
根本数据类型 援用数据类型
Number Object
String Function
Boolean Array
Undefind Date
Null RegExp
Symbol(ES6 新增) Math
BigInt(ES10 新增) … 都是 Object 类型的实例对象
  • 根本数据类型和援用数据类型的贮存形式区别:

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

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

例如:

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

obj 在内存中的贮存如下:

栈内存 栈内存 堆内存
name val val
a 100
b ‘name’
c AAAFFF000(一个援用地址,指向堆内存的值) [10,20,30]
d BBBFFF000(一个援用地址,指向堆内存的值) {x:10}

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

浅拷贝

何为浅拷贝?当 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 的扭转。

常见的浅拷贝

咱们以上面的对象为例:

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');    
    }
}

办法一:间接赋值

间接赋值的办法就是咱们方才所举的例子????,这种形式实现的就是纯正的浅拷贝,obj2 的任何变动都会反映在 obj 上。

办法二:应用对象的解构

let obj2 = {...obj}

办法三:应用循环

对象循环咱们应用 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)

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

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

浅拷贝总结:

办法一:就是纯正的浅拷贝,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 而变动了

这几种拷贝办法无奈满足更深层级的拷贝,所以咱们须要另一种万全之策 –深拷贝

深拷贝

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

let obj2 = JSON.parse(JSON.stringify(obj));

obj2.schoole.name= 'susan';
console.log(obj.school.name); // 'cherry'
//obj 中属性值并没有扭转, 阐明是深拷贝

这种办法是比较简单的深拷贝,在对象属性的类型比较简单的时候,咱们能够采取这种办法疾速深拷贝。

但当对象属性的类型较为简单时,就会发现这种办法尽管能实现深拷贝,但也有很多坑,运行下面的代码后发现:

  • 值为 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 序列化规定的形容,供大家参考:

对大多数简略值来说,JSON 字符串化和 toString()的成果基本相同,只不过序列化的后果总是字符串:

JSON.stringify(42); // “42”

JSON.stringify(“42”); // “”42″”(含有双引号的字符串)

JSON.stringify(null); // “null”

JSON.stringify(true); // “true”

所有平安的 JSON 值(JSON-safe)都能够应用 JSON.stringify(…)字符串化。平安的 JSON 值是指可能出现为无效 JSON 格局的值。

为了简略起见,咱们来看看什么是不平安的 JSON 值。undefined、function、symbol(ES6+)和蕴含循环援用(对象之前互相援用,造成一个有限循环)的对象都不合乎 JSON 构造规范,其余反对 JSON 的语言无奈解决它们。

JSON.stringify(…)在对象中遇到 undefined、function 和 symbol 时会主动将其疏忽,在数组中则会返回 null(以保障单元地位不变)。

例如:

JSON.stringify(undefined); //undefined

JSON.stringify(function(){}); //undefined

JSON.stringify([1,undefined,function(){},4]); //”[1, null, null, 4]”

JSON.stringify({a:2, b: function(){}}); //”{“a”: 2}”

对蕴含循环援用的对象执行 JSON.stringify(…); 会报错。

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

办法二:手写 deepClone

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

用过 lodash 的小伙伴都晓得 lodash 提供了_.cloneDeep 办法深克隆,想看 lodash 实现源码的能够点击这里,它的源码里实现的比较复杂,思考的状况比拟多,咱们写一个简略版的深拷贝能够在本人我的项目中应用即可。

简略的实现思路:

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 深拷贝相干代码,置信你会对深拷贝有进一步的了解~

结语

作者齐小神,前端程序媛一枚。

有点文艺,喜爱摄影。尽管当初朝九晚五,埋头苦学,但幻想是做女侠,扶贫济穷,仗剑走咫尺。心愿有一天能改完 BUG 去实现本人的幻想。

公众号:大前端 Space,不定时更新,欢送来玩~

退出移动版