在编码的过程中,难免会遇到需要深拷贝一个对象的时候,这个时候,如果项目中没有深拷贝方法,而且也不允许使用三方库,例如lodash,就需要手动实现一个深拷贝功能了。
那么,什么是深拷贝呢?与深拷贝对应的,还有浅拷贝。
浅拷贝:
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了属性的值,就会影响到另一个对象。
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
下面我们进入正题。
在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。
JSON.parse(JSON.stringify());
这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。
如果对象只有一层,那么我们很容易写出以下代码:
export function clone(target) {
const cloneTarget = {}
for (let key in target) {
cloneTarget[key] = target[key]
}
return cloneTarget
}
但是,在实际开发中,我们要拷贝的对象往往不止一层,可能有很多层,这个时候,聪明的你肯定想到了,用递归!于是,我们又能写出如下代码:
target是否是对象?
——是:用递归进行拷贝
——否:直接返回
export function cloneDeep(target) {
if (isObject(target)) {
const cloneTarget = {}
for (let key in target) {
cloneTarget[key] = cloneDeep(target[key])
}
return cloneTarget
} else {
return target
}
}
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
于是,我们用下面的对象测试一下:
const target = {
a: 1,
b: [ 1, 2, { x: 1, y: [1, 2, 3] }, 4 ],
c: {
apple: 1,
orange: 2,
verb: {
angry: 1,
smile: 2,
},
},
}
const result = deepClone(target)
console.log(result)
可以看到:
本来应该是数组的target.b,现在变成了对象。于是乎,你马上发现了上面代码的问题,没有对数据进行判断。
于是,你很快又进行了改写,判断一下target是否为数组:
target是否为数组?
——是:cloneTarget初始化为[]
——否:cloneTarget初始化为{}
export function cloneDeep(target) {
if (isObject(target)) {
const isArray = Array.isArray(target)
const cloneTarget = isArray ? [] : {}
for (let key in target) {
cloneTarget[key] = cloneDeep(target[key])
}
return cloneTarget
} else {
return target
}
}
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
再次运行,终于正确地拷贝了数组:
我们执行一下下面的测试用例:
const target = {
a: 1,
b: [ 1, 2, { x: 1, y: [1, 2, 3] }, 4 ],
c: {
apple: 1,
orange: 2,
verb: {
angry: 1,
smile: 2,
},
},
}
target.d = target
const result = cloneDeep(target)
console.log(result)
会发现,浏览器报错了:
很明显,因为递归进入死循环导致栈内存溢出了。
原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况。
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储 key-value
形式的数据,且 key
可以是一个引用类型,我们可以选择 Map
这种数据结构。
map 中是否已经拷贝过的对象?
——有,直接返回该对象
——没有,将当前对象作为 key,克隆对象作为 value 储存起来,继续拷贝
根据上面的思路,我们可以写出如下代码:
export function cloneDeep(target, map = new Map()) {
if (isObject(target)) {
const isArray = Array.isArray(target)
const cloneTarget = isArray ? [] : {}
if (map.has(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
for (let key in target) {
cloneTarget[key] = cloneDeep(target[key], map)
}
return cloneTarget
} else {
return target
}
}
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
接下来,我们可以使用 WeakMap 代替 Map :
export function cloneDeep(target, map = new WeakMap()) {
// ...
}
使用 WeakMap 有什么好处呢?我们先来看看 WeakMap 的作用:
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
什么是弱引用?
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
我们默认创建一个对象:const obj = {}
,就默认创建了一个强引用的对象,我们只有手动将obj = null
,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。
举个例子,如果我们使用 Map
的话,那么对象间是存在强引用关系的:
let obj = { name : 'MZHC'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;
虽然我们手动将 obj
,进行释放,然是 target
依然对 obj
存在强引用关系,所以这部分内存依然无法被释放。
再来看 WeakMap
:
let obj = { name : 'MZHC'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;
如果是 WeakMap
的话,target
和 obj
存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
设想一下,如果我们要拷贝的对象非常庞大时,使用 Map
会对内存造成非常大的额外消耗,而且我们需要手动清除 Map
的属性才能释放这块内存,而 WeakMap
会帮我们巧妙化解这个问题。
上面的代码中,我们使用了 for...in 这种方式,实际上, for...in 的遍历效率是比较低的,如果对象层数很多,则效率会大打折扣,我们采用 for 循环来改写一下:
export function cloneDeep(target, map = new WeakMap()) {
if (isObject(target)) {
const isArray = Array.isArray(target)
const cloneTarget = isArray ? [] : {}
if (map.has(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
if (isArray) {
forEach(target, (item, index) => {
cloneTarget[index] = cloneDeep(item, map)
})
} else {
const keys = Object.keys(target)
forEach(keys, key => {
cloneTarget[key] = cloneDeep(target[key], map)
})
}
return cloneTarget
} else {
return target
}
}
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
function forEach(array, fn) {
// 这里需要使用len记录一下数组的长度
for (let i = 0, len = array.length; i < len; i++) {
fn(array[i], i)
}
}
这里采用 while 循环也是可以的,lodash 的深拷贝这里是采用while循环实现的,效率上其实和for循环差不多。
在上面的代码中,我们其实只考虑了普通的 object
和 array
两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型。
首先,判断是否为引用类型,我们还需要考虑 function
和 null
两种特殊的数据类型:
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
我们可以使用 toString 来获取准确的引用类型。
每一个引用类型都有
toString
方法,默认情况下,toString()
方法被每个Object
对象继承。如果此方法在自定义对象中未被覆盖,toString()
返回"[object type]"
,其中 type 是对象的类型
注意,上面提到了如果此方法在自定义对象中未被覆盖,toString
才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp
等都重写了toString
方法。
我们可以直接调用Object
原型上未被覆盖的toString()
方法,使用call
来改变this
指向来达到我们想要的效果。
function getTag(target) {
return Object.prototype.toString.call(target)
}
下面我们抽离出一些常用的数据类型以便后面使用:
// 可迭代类型
const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'
const argsTag = '[object Arguments]'
// 不可迭代类型
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const numberTag = '[object Number]'
const regexpTag = '[object RegExp]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const funcTag = '[object Function]'
const weakMapTag = '[object WeakMap]'
在上面的集中类型中,我们简单将他们分为两类:
我们分别为它们做不同的拷贝。
上面我们已经考虑的object
、array
都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有Map
,Set、argument
等都是可以继续遍历的类型,这里我们只考虑这几种,如果你有兴趣可以继续探索其他类型。
对于object和argument,我们可以使用Object.create(Object.getPrototypeOf(Object(target)))的方法去创建初始化的克隆对象,对于map和set,我们可以使用new target.constructor的方式去创建初始化的克隆对象。
function getInit(target, tag) {
const Ctor = target.constructor
switch (tag) {
case objectTag:
case argsTag:
return objectCreate(Object.getPrototypeOf(Object(target)))
case arrayTag:
return initCloneArray(target)
case mapTag:
case setTag:
return new Ctor
}
}
对于object和argument, 如果直接使用new的方式,这样会调用一次构造函数,有一定的性能浪费,所以使用Object.create的方式,这样会生成一个空对象,不需要调用构造函数,Object.create会把target的原型自动关联到这个空对象上面。
对于map和set,不能直接使用Object.create的方式,因为这样生成的map或者set对象会丢失属性,需要使用new来创建初始化的克隆对象。
const map = new Map();
const set = new Set();
const map1 = Object.create(Object.getPrototypeOf(Object(map)));
const set1 = Object.create(Object.getPrototypeOf(Object(set)));
const map2 = new map.constructor();
const set2 = new set.constructor();
console.log(map1);
console.log(set1);
console.log(map2);
console.log(set2);
这个时候如果使用map1.set('name', 'mzhc'),还会报错:
对于数组,需要考虑正则表达式的exec函数执行后产生的数组,exec函数匹配到结果后返回的数组中多了index和input两个属性:
const reg = /\d(mzhc)\d/;
const result = reg.exec('520mzhc1314');
console.log(result);
因此我们需要对正则表达式exec方法返回的数组进行克隆初始化:
function initCloneArray(array) {
const length = array.length
const result = new Array(length)
// Add properties assigned by `RegExp#exec`.
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:
Boolean
、Number
、String
、Date
这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:
function cloneNonIterative(target, tag) {
const Ctor = target.constructor
switch (tag) {
case numberTag:
case stringTag:
return new Ctor(target)
case boolTag:
case dateTag:
return new Ctor(+target)
case regexpTag:
return cloneReg(target)
case symbolTag:
return cloneSymbol(target)
}
}
克隆Symbol:
function cloneSymbol(symbol) {
return Object(Symbol.prototype.valueOf.call(symbol))
}
克隆正则:
function cloneReg(target) {
const reFlags = /\w*$/
const result = new target.constructor(target.source, reFlags.exec(target))
result.lastIndex = target.lastIndex
return result
}
在lodash源码中,如果是error、function和WeakMap类型,首先会判断一下如果不存在父对象,就直接返回{},如果存在父对象,这直接返回父对象中该值的引用。
这里,我们也采用相同的方式:
const cloneableTags = {}
cloneableTags[funcTag] = cloneableTags[errorTag] = cloneableTags[weakMapTag] = false
export function cloneDeep(target) {
return baseClone(target)
}
function baseClone(target, map = new WeakMap(), parentObj) {
/* ... */
let cloneTarget = {}
/* ... */
if (!cloneableTags[tag]) {
return parentObj ? target : {}
}
/* ... */
return cloneTarget
}
/**
* 深拷贝
*/
const objectProto = Object.prototype
const objectToString = objectProto.toString
const objectCreate = Object.create
const getPrototype = Object.getPrototypeOf
const hasOwnProperty = objectProto.hasOwnProperty
// 可迭代类型
const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'
const argsTag = '[object Arguments]'
// 不可迭代类型
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const numberTag = '[object Number]'
const regexpTag = '[object RegExp]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const funcTag = '[object Function]'
const weakMapTag = '[object WeakMap]'
// 可迭代类型
const iterableTags = {}
iterableTags[mapTag] = iterableTags[setTag] =
iterableTags[arrayTag] = iterableTags[objectTag] =
iterableTags[argsTag] = true
// 可拷贝类型
const cloneableTags = {}
cloneableTags[mapTag] = cloneableTags[setTag] =
cloneableTags[arrayTag] = cloneableTags[objectTag] =
cloneableTags[argsTag] = cloneableTags[boolTag] =
cloneableTags[dateTag] = cloneableTags[numberTag] =
cloneableTags[regexpTag] = cloneableTags[stringTag] =
cloneableTags[symbolTag] = true
// 不可拷贝类型
cloneableTags[funcTag] = cloneableTags[errorTag] = cloneableTags[weakMapTag] = false
/**
* 深拷贝入口函数
* @param target 要进行深拷贝的目标对象
* @returns 深拷贝完成后的对象
*/
export function cloneDeep(target) {
return baseClone(target)
}
/**
*
* @param target 要进行深拷贝的目标对象
* @param {WeakMap} map 储存已经拷贝过的对象,
* @param parentObj `target`的父对象
* @returns
*/
function baseClone(target, map = new WeakMap(), parentObj) {
// 原始类型
if (!isObject(target)) {
return target
}
// 获取类型
const tag = getTag(target)
// 函数、Error和WeakMap
if (!cloneableTags[tag]) {
return parentObj ? target : {}
}
// 初始化
let cloneTarget = {}
if (iterableTags[tag]) {
cloneTarget = getInit(target, tag)
} else {
return cloneNonIterative(target, tag)
}
// 防止循环引用引起的栈内存溢出
if (map.has(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
// 克隆set
if (tag === setTag) {
target.forEach((value) => {
cloneTarget.add(baseClone(value, map, target))
})
return cloneTarget
}
// 克隆map
if (tag === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, baseClone(value, map, target))
})
return cloneTarget
}
// 克隆数组
if (tag === arrayTag) {
arrayEach(target, (item, index) => {
cloneTarget[index] = baseClone(item, map, target)
})
return cloneTarget
}
// 克隆纯对象或者参数数组
if (tag === objectTag || tag === argsTag) {
const keys = Object.keys(target)
arrayEach(keys, key => {
cloneTarget[key] = baseClone(target[key], map, target)
})
return cloneTarget
}
return cloneTarget
}
/**
* 检查是否是对象
* @param obj
* @returns
*/
function isObject(obj) {
return obj !== null && (typeof obj === 'object' || typeof obj === 'function')
}
/**
* 使用`Object.prototype.toString`将一个值转换为string
* @param target 需要转换的值
* @returns {string} 返回转换后的值
*/
function getTag(target) {
return objectToString.call(target)
}
/**
* 根据被克隆对象的类型初始化克隆对象
* @param target 被克隆的目标对象
* @param tag 被克隆的目标对象的类型
* @returns 初始化后的克隆对象
*/
function getInit(target, tag) {
const Ctor = target.constructor
switch (tag) {
case objectTag:
case argsTag:
return objectCreate(getPrototype(Object(target)))
case arrayTag:
return initCloneArray(target)
case mapTag:
case setTag:
return new Ctor
}
}
/**
* 初始化数组克隆
* @param array 被克隆的数组
* @returns 初始化后的克隆数组
*/
function initCloneArray(array) {
const length = array.length
const result = new Array(length)
// Add properties assigned by `RegExp#exec`.
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
/**
* 遍历数组的工具函数
* @param {Array} array 遍历的目标数组
* @param {Function} fn 每次迭代调用的函数
*/
function arrayEach(array, fn) {
for (let i = 0, len = array.length; i < len; i++) {
fn(array[i], i)
}
}
/**
* 克隆不可迭代类型
* @param target
* @param tag
* @returns
*/
function cloneNonIterative(target, tag) {
const Ctor = target.constructor
switch (tag) {
case numberTag:
case stringTag:
return new Ctor(target)
case boolTag:
case dateTag:
return new Ctor(+target)
case regexpTag:
return cloneReg(target)
case symbolTag:
return cloneSymbol(target)
}
}
/**
* 克隆正则表达式
* @param target 被克隆的正则表达式
* @returns 克隆后的正则表达式
*/
function cloneReg(target) {
const reFlags = /\w*$/
const result = new target.constructor(target.source, reFlags.exec(target))
result.lastIndex = target.lastIndex
return result
}
/**
* 克隆Symbol
* @param symbol 被克隆的symbol
* @returns 克隆后的symbol
*/
function cloneSymbol(symbol) {
return Object(Symbol.prototype.valueOf.call(symbol))
}
lodash
如何写出一个惊艳面试官的深拷贝? - 掘金