趁着周五没那么忙,抽个空整理一下最近使用比较频繁的一个小技术 对象的深复制
。
感觉啊,这个标题和今天的节日(假装不知道原来是情人节)那么遥相呼应。啊,没有女朋友?没有女朋友没关系,复制一个啊。先走你一个:console.log('GirlFriend')
。
同时笔者需要预个警,本篇注重实际中的应用情况,中规中矩,不搞花里胡哨。所以主要是对数组
和字面量对象
进行讨论,毕竟前端开发中遇到最多的对象就是这两个了。在正式分析对象的深复制这个问题之前,笔者觉得有必要对在座的朋友科普下必要的知识。
科普区
JavaScript中数据类型分为哪几种?
这个问题在面试中的命中率真的挺高,而且也是本篇的重要所在。了解的朋友可跳过继续向下看,不了解的朋友请跟着我慢慢理解。
JavaScript中数据类型分为基本数据类型
和引用数据类型
(当然还有别的叫法,只是笔者认为这种叫法更科学)。
基本数据类型
: null, undefined, int, string, boolean
引用数据类型
: {'a':'b'}(字面量对象
), Array, Map, Set, Symbol 等等...其实就是基本数据类型以外的类型
而且,不同的数据类型在内存中的存储方式也是不同的:
基本数据类型在内存中的存储方式
我们都知道,JavaScript中的内存分为栈内存
和堆内存
(不知道?请点击)。基本数据类型
将变量和值一起放在栈内存;引用数据类型
则将变量放在栈内存而将值放在堆内存。先来讲基本数据类型
。
基本数据类型在JavaScript编程中用得极为广泛,比如:
let a = 1;
let b = 'name';
let c = true;
let d = null;
let e = undefined;
这些都属于基本数据类型,都存储在栈内存中;如以下形式:
栈内存中的变量数据有个好处是如果需要获取某个变量值的copy值很方便,直接通过 =
操作即可,不需要考虑额外的问题。比如:let f = a;
这个操作意在拷贝一份 a
这个变量,此时栈内存中就变成这样了:
多出一个变量 f
,即使说是从 a
复制而来的,结果却和 a
没有任何关系。用代码验证一下:
let a = 1;
let f = a;
//此时改变a的值
a = 2;
console.log(`a 是${a}`);
console.log(`f 是${f}`);
运行结果:
引用数据类型在内存中的存储方式
上面说到:引用数据类型则将变量放在栈内存而将值放在堆内存
。该怎么理解?没图我说个jb?
假设有个变量person:
let person={
'name':'Mario'
}
在内存中是这样的
栈内存中的变量 person
指向堆内存中一块内存(相当于持有该内存的指针
),而那块内存中存储 person 变量的相关内容。
因此可以看出引用数据类型的复制并没有基本数据类型来得方便。即便如此,我们还是来证实下这个想法:
let person={
'name':'Mario'
}
let person_copy = person;
//修改person中的内容
person['name']='JavaScript';
console.log(`person: ${person['name']}`);
console.log(`person_copy: ${person_copy['name']}`);
运行结果:
可以看出当我们复制好一份 person_copy
,并对 person
进行了一次修改。结果两个变量同时变化。为什么?
因为当程序进行到let person_copy = person;
这里的时候,并不是直接把 person 的内容赋值给变量 person_copy,而是把 person 的指针
赋值给了变量 person_copy。
所以说变量 person 和 person_copy 都持有了该内存块的指针,因此不管哪个变量修改了内存中的内容,另一个变量对应的值也会变化。最终我们可以确定:如果想复制一个引用类型的数据,就是要将该内存块中的内容复制进另一个内存块中并把新的指针赋值给新变量
。
大概内容已经科普完了,接下来就开始本文的重点内容
对象的(深)复制
在开发过程中,如果某条数据(尤其是从后台请求回来的数据)使用频率很高而且用途复杂,那么就不得不为它进行一次复制以备不时之需。针对使用频率较高的两个对象数组
和字面量对象
,我们开始逐一讨论。
数组的复制
数组在实际开发中,元素主要分为基本数据类型
和字面量对象
(不排除还有别的类型数据,只是笔者没遇到过,所以只针对普遍的情况)。
针对元素是基本数据类型的数组的复制操作,笔者提供4种方法:
- ES6的对象扩展运算符 [...]
let origin = [1, 2, 3, 4, 5];
let another = [...origin];
//向原数组中添加一个元素
origin.push(6);
console.log(`another元素: ${another}`);
运算结果:
- slice
let origin = [1, 2, 3, 4, 5];
let another = origin.slice();
//向原数组中添加一个元素
origin.push(6);
console.log(`another元素: ${another}`);
运行结果:
- concat
let origin = [1, 2, 3, 4, 5];
//相当于向origin中拼接一个空数组
let another = origin.concat([]);
//向原数组中添加一个元素
origin.push(6);
console.log(`another元素: ${another}`);
运行结果:
- JSON.stringify
这个方法可行但是几乎没人这么用。原理是将数组转为字符串再转回数组类型。
let origin = [1, 2, 3, 4, 5];
let another = JSON.parse(JSON.stringify(origin));
//向原数组中添加一个元素
origin.push(6);
console.log(`another元素: ${another}`);
运行结果:
其中笔者觉得第一和第二个方法比较好用。不过如果数组中的元素是字面量对象的话,请继续向下看。
字面量对象的深复制
实际开发中,所谓字面量对象可以人为是一段json数据。json的重要性不用说,那么相当重要,前后端交互的核心。
先给出一段json:
{
'name': 'Mario',
'age': 26,
'isCoder': true,
'homeWebPage': null,
'fullStackSkills': undefined,
'hobbies': ['LoL', 'Travel', 'Coding'],
'phone': {
'home': 123321,
'office': 456654
}
}
首先分析这段json中的数据,不管是基本数据类型还是引用数据类型都有了,所以想要复制这个json对象,我们需要针对不同的数据做不同的处理。根据科普的知识我们已经知道,复制基本数据类型可以直接赋值,对于引用数据类型则不行,而且主要使用到的数组和字面量对象(这里也可以认为是json)这两个类型数据的复制方法都不同。因此我们首先要判断某一个数值是否是对象:
//判断item是否为'object';该方法主要是为了区分参数是基本类型还是引用类型
function isObject(item) {
return (item === null || item === undefined) ? false : (typeof item === 'object');
}
其次,我们可以看到origin是一段json,orgin中的phone字段对应的值也是一段json,所以要想整个复制这个对象难免要用到递归
,一层一层嵌套进行。奉上核心代码:
/**
*
* @param {字面量对象} origin
* @param {origin的镜像对象} mirror
*/
function deepClone(origin, mirror) {
//获取该字面量对象的所有的key
let keys = Object.keys(origin);
//遍历所有的key已保证复制的完整
keys.forEach(key => {
let value = origin[key];
if (isObject(value)) { //判断是否为对象,如果是则需要额外处理;如果不是则直接复制
if (Array.isArray(value)) { //判断是否为数组,如果是则需要复制该数组并存入mirror;如果不是则进行递归调用
let copy = value.slice();
mirror[key] = copy;
} else {
//初始化本次字面量对象的镜像对象
let obj = {};
mirror[key] = obj;
//引用传值
//递归调用
deepClone(value, obj);
}
} else {
mirror[key] = value;
}
});
}
通过测试,
//Test
let mirror = {};
deepClone(origin, mirror);
//向原对象中的hobbies中增加一项
origin['hobbies'].push('Eat');
console.log(mirror);
运行结果:
证明方法有效。
当然针对json数据的复制,也可以只用JSON.parse(JSON.stringify(origin))实现,具体效率怎么样,笔者也没有进行测试。所以这块有待验证。因为该文章注重实际开发中的应用,所以例子没有用到复杂的对象(例如:Set, Symbol 等等...)。所以如果有这个疑问的朋友也不用纠结了。
写得差不多了,能想到了就这些了...收拾收拾准备跑路了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。