前提: 假设您已经知道为何在JavaScript中须要深拷贝和浅拷贝了。
举两个例子:javascript
const a = [1, 2, { key: 20 }] const b = [...a] b[2].key = 30 console.log(a[2] === b[2]) console.log(a === b) // true const o = { k1: { kk1: 50} } const o1 = { ...o } o1.k1.bb = { bb: 30 } console.log(o1.k1 === o.k1) // true
在上面数组和对象中分别改变了 b[2]
和 o1.k1
,可是最后结果的获得和原来的值保持一致。java
在JavaScript中分为2大类(原始值类型和对象类型)7中数据类型(Boolean, Null, Undefined, Number, String, Symbol),原始值类型标识对这个数据的任何操做都会返回一个新的数据,也就是说一旦申明一个原始值类型的数据则该数据不可变。若是申明一个对象类型例如:{}
,new Map()
,new Set()
,new Regex()
,new Date()
等等。再进一步来讲:
(网图,侵删)git
不一样的类别的数据保存的数据结构,数据申明保存在栈数据结构中,而对象应用则分配在堆中,即用一大块非结构化的内存区域
参考: https://developer.mozilla.org...
咱们通常说的深拷贝和浅拷贝主要是对数组和对象来进行的,下面也主要对数组和对象进行实践操做:github
浅拷贝能够当成是单层拷贝,何为单层拷贝,就是复制的对象深度只有一。json
以下操做:数组
const arr = [1, 2, 3] const newArr = [].concat(arr) // arr === newArr false
上面这个方式就直接把把arr合并到新的数组中,并把新的数组返回回来,达到拷贝的目的。数据结构
数组的复制操做:ide
const arr = [1, 2, 3] const newArr = arr.slice() // arr === newArr false
上面从0 -> arr.length - 1 进行拷贝复制,返回一个新的数组.函数
数组的建立方式,经过给定一个不定参数,而后建立一个数组工具
const arr = [1, 2, 3] const newArr = Array.from(arr) // newArr === arr false
经过原有数组建立新数组,获得拷贝目的。
上面三种方式均可以简单进行数组的浅拷贝,若是数组内嵌套有其余数据呢?这个数据是没有处理过呢,如何作呢?且看下文
对象合并方法:
const o = {a: 1, b: 2} const no = Object.assign({}, o) // o === no1 false
经过一个新对象和原有对象合并,获得新的对象
扩展符号以下:
const o = { a: 1, b: 1 } const o1 = {...o } // o === o1 false
经过一个新的对象申明,并把原有对象属性经过 ...
复制下来,达到拷贝目的。
上文中都是单层数据拷贝,在内存堆栈来讲,就是在栈内从新从新开辟的空间,可是实际上,这个对象对应的二层对象并无进行任何处理,依旧仍是原有只想,浅拷贝实现的示意图以下:
红色部分是新进行申明的变量以及新的在堆中的内容,绿色部分老是没有被复制。如何始终让绿色能够被拷贝,被复制呢?下面就说一下这个
经过v8提供的JSON序列化和反序列的的方法,首先把json转换成字符串,在js中,全部Primitive 值都是不可变的,一旦修改就是新的数据。而后经过反序列的方式,直接将JSON.parse 转换回来了便可。
const a = { ... } const deepCloneA = JSON.parse(JSON.stringify(a))
JSON 序列化和反序列化局限:
https://developer.mozilla.org...
局限也是 JSON.stringify
的局限。
那么总结一下,若是咱们要进行深拷贝,须要考虑的问题是那些呢?
见以下代码:
function deepCopy(o) { if (typeof o !== 'object') return o; const object = {}; for (const key in o) { if (o.hasOwnProperty(key)) { const element = o[key]; if (typeof element === 'object' && element !== null) deepCopy(element); else object[key] = element; } } return object }
测试代码:
const o = { a: 2, b: '2', c: { say: 'hello world' }, d: null, e: undefined, f: function() { console.log('Good') }, symbol: Symbol('hello') } console.log(o) const o1 = deepCopy(o) console.log(o1);
输出以下:
{ a: 2, b: '2', c: { say: 'hello world' }, d: null, e: undefined, f: [Function: f], symbol: Symbol(hello) } { a: 2, b: '2', d: null, e: undefined, f: [Function: f], symbol: Symbol(hello) } false
在代码里面,咱们使用递归,实现了基本数据的复制。上面的状况基本可以解决咱们大部分
在上述的图中,若是咱们的数据结构变成这样,结果是怎么样的呢?须要对一些特别的数据进行处理, 例如Date, Map 等。这里以Date和Map为例子,其余相似:
最后获得两个值都是空值,因此须要对写类型的数据进行特别处理.
这里增长一种工具类:
const objectTag = '[object Object]'; const arrayTag = '[object Array]'; const dateTag = '[object Date]'; const mapTag = '[object Map]'; const getTag = (o) => Object.prototype.toString.call(o)
开始真正的表演:
function deepCopy(o) { if (typeof o !== 'object') return o; const object = {}; for (const key in o) { const element = o[key]; if (element && typeof element === 'object') { const tag = getTag(element); const Ctor = element.constructor switch (tag) { case arrayTag: case objectTag: object[key] = deepCopy(element); break case dateTag: object[key] = new Ctor(+element) break case mapTag: const map = new Ctor element.forEach((subValue, key) => { map.set(key, deepCopy(subValue)) }) object[key] = map default: break; } } else object[key] = element; } return object; }
运行相同的测试代码,输出以下:
{ a: 2, b: '2', c: { say: 'hello world' }, d: null, e: undefined, f: [Function: f], g: Infinity, symbol: Symbol(hello), dd: 2019-10-22T12:54:57.976Z, cc: Map { 'a' => 2 } } { a: 2, b: '2', c: { say: 'hello world' }, d: null, e: undefined, f: [Function: f], g: Infinity, symbol: Symbol(hello), dd: 2019-10-22T12:54:57.976Z, cc: Map { 'a' => 2 } } false false
这里就处理好了一些特殊数据的问题。
从上面也能够获得,一个数据的要想支持深拷贝,必需要对对应深拷贝的数据进行处理, 上面也是 lodash深拷贝实现思路。
特殊数据也处理完成后,若咱们有下面数据:
直接运行看状况:
这里须要对代码进行一些处理,咱们须要判断代码是否存在循环引用呢?咱们在递归时候,不断把当前父级(currentParent),固然的复制的数据(object), 还有最原始的数据(o)传入,是否是能够经过循环判断是否存在递归, 下面实现一下:
const objectTag = '[object Object]'; const arrayTag = '[object Array]'; const dateTag = '[object Date]'; const mapTag = '[object Map]'; const getTag = (o) => Object.prototype.toString.call(o); function deepCopy(o, parent = null) { if (typeof o !== 'object') return o; const object = {}; let _parent = parent while(_parent) { if (_parent.originParent === o) { return _parent.currentParent } _parent = _parent.parent } for (const key in o) { const element = o[key]; if (element && typeof element === 'object') { const tag = getTag(element); const Ctor = element.constructor; switch (tag) { case arrayTag: case objectTag: object[key] = deepCopy(element, { parent, currentParent: object, originParent: o }); break; case dateTag: object[key] = new Ctor(+element); break; case mapTag: const map = new Ctor(); element.forEach((subValue, key) => { map.set(key, deepCopy(subValue, { parent, currentParent: object, originParent: o })); }); object[key] = map; default: break; } } else object[key] = element; } return object; }
咱们在入口时候判断,若是是递归的话,就把当前复制的结果给返回便可。查看以下示例:
const o = { a: 2, b: '2', c: { say: 'hello world' }, c1: { say: 'good idea' }, d: null, e: undefined, f: function() { console.log('Good'); }, g: Infinity, symbol: Symbol('hello'), dd: new Date(), cc: new Map([['a', 2]]), }; o.ff = o; o.cc.set('cir', o) o.c.bb = o.c1
输出结果:
{ a: 2, b: '2', c: { say: 'hello world', bb: { say: 'good idea' } }, c1: { say: 'good idea' }, d: null, e: undefined, f: [Function: f], g: Infinity, symbol: Symbol(hello), dd: 2019-10-24T05:17:09.550Z, cc: Map { 'a' => 2, 'cir' => [Circular] }, ff: [Circular] } { a: 2, b: '2', c: { say: 'hello world', bb: { say: 'good idea' } }, c1: { say: 'good idea' }, d: null, e: undefined, f: [Function: f], g: Infinity, symbol: Symbol(hello), dd: 2019-10-24T05:17:09.550Z, cc: Map { 'a' => 2, 'cir' => [Circular] }, ff: [Circular] } false false false
这样就保证递归的正确性了。
或许这里递归方式并非解决重复引用的最好方法,也有方式采用 WeakMap 方式来解决,每次递归的时候都用WeakMap存下便可。
深浅拷贝涉及JS的数据类型的存储机制,因此对深浅拷贝能够明确区分在JS中 原始类型(Primitive) 或者 对象类型(Object) 存储的区分。
若有问题,欢迎交流。
源码地址: https://github.com/zsirfs/content-scripts/blob/master/deep-copy.js