前端日常开发常用功能系列之拷贝
这是前端日常开发常用功能这个系列文章的第一篇,该系列是日常开发中一些常用的功能的再总结、再提炼,以防止遗忘,便于日后复习。该系列预计包含以下内容: 防抖、节流、去重、拷贝、最值、扁平、偏函数、柯里、惰性函数、递归、乱序、排序、注入、上传、下载、截图。。。
在日常开发中,经常会有对象拷贝的需求
浅拷贝
浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响
实现方法
const obj = { a: 'a' }; let temp = obj; // 其实这样的简单赋值对于复杂数据类型来说就是浅复制 // 带来的后果 temp.a = 'aa'; obj.a // 'aa' // 可以看到在temp中更改对象属性也会反应到obj中,原因很简单: 因为obj和temp引用的是同一个对象
深拷贝
深复制不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深复制的对象的引用图不包含任何原有对象或对象图上的任何对象,复制后的对象与原来的对象是完全隔离的
这里可以循序渐进,从比较简单的情况谈起
实现方法
情况一: 简单数组的拷贝(这里“简单数组”指的是一维的,数组项都是简单数据类型)
const arr = [1, 3, 1, 4, 'love', 'you']; const temp = arr.concat(); const temp1 = arr.slice(); arr === temp; // false arr === temp1; // false // 利用数组的原型方法concat和slice方法
但是concat、slice方法并没有对数组项进行深度拷贝,如果数组项中一旦包含复杂数据类型,就只是拷贝了该项的引用到一个新数组中,可以看一下
const arr = [1, 3, 1, 4, 'love', 'you', {girl: 'Lucy'}]; const temp = arr.concat(); const temp1 = arr.slice(); temp[temp.length - 1].girl = 'Lily'; arr[arr.length - 1] // {girl: 'Lily'} temp1[temp1.length - 1] // {girl: 'Lily'}
情况二: 包含复杂数据类型的数组,同时数组项及其子项不包含函数、正则; 对象,对象的属性不包含函数、正则
const arr = [1, 3, 1, 4, 'love', 'you', {girl: 'Lucy'}]; const obj = {boy: 'Jack', cars: ['Toyota', 'Ford']}; const copyArr = JSON.parse(JSON.stringify(arr)); const copyObj = JSON.parse(JSON.stringify(obj)); // 可以测试一下 arr === copyArr // false obj === copyObj // false copyArr[copyArr.length - 1].gril = 'Lily'; arr[arr.length - 1]; // {girl: 'Lucy'} copyObj.boy = 'John'; obj.boy; // 'Jack'
该方法使用了JSON对象的stringify和parse方法进行对象的序列化和反序列化来实现对象的深度拷贝
但是这个方法并不是万能的,因为对于对象的属性或者数组项中包含函数、正则,或者出现循环引用的时候,该方法就不再有用
const obj = {boy: 'Jack', say: function() { return 'Jack' }, reg: /sw+/i}; const copyObj = JSON.parse(JSON.stringify(obj)); // copyObj的结果: // {boy: "Jack", reg: {}} const parent = {}; const son = {}; parent.child = son; son.parent = parent; JSON.stringify(parent); // 抛出错误: Uncaught TypeError: Converting circular structure to JSON
可以看到遇到函数序列化时会忽略该属性,正则则只会被简单转为{},循环引用stringify方法则会抛出错误,Data类型使用JSON.parse则只会得到一个时间字符串。
情况三: 怎么实现一种方法,能够普遍使用来进行深度拷贝呢?
思路:
- 如果要拷贝的对象时简单数据类型,则直接返回
- 如果要拷贝的对象是复杂数据类型,则对其进行遍历
- 如果遍历项为基本数据类型,则直接进行赋值
- 如果遍历项为复杂数据类型,则进行递归
具体实现:
const util = (function() { const getType = obj => { return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); }; const isType = (obj, type) => { return getType(obj) === type; }; return { isType, getType, }; })(); const deepCopy = obj => { const objType = util.getType(obj); console.log(objType); if(objType === 'null' || (objType !== 'object' && objType !== 'array')) { return obj; } let target = objType === 'object' ? {} : []; // 设置容器类型 for(let key in obj) { const val = obj[key]; if(util.isType(val, 'array') || util.isType(val, 'object')) { target[key] = deepCopy(val); } else { target[key] = val; } } return target; };
总结: js实现复杂数据类型的深拷贝实际上没有什么捷径,就是遍历、递归,直到最底层的属性不是数组或者对象然后进行赋值。
其实上面的方法也是无法对对象属性中包含函数、日期对象、正则时进行完全深度拷贝的,但是在实际的应用中,几乎没有什么场景会有这种需求(至少我还没遇到过。。。)。