这部分我是看了 lodash 的相关源码,它真的实现得非常完整!
总结成一句话,就是需要考虑很多数据类型,然后针对这些数据类型拷贝就行了😏
对于引用类型来说,我们基本可以按照以下思路走:
- 初始化。即调用相应的构造函数
- 递归地赋值
- 有循环引用的话需要处理一下
1. 拷贝基本类型
基本类型直接赋值就可以。
1 2 3 4 5 6 7 8 9 10 11 12
| function deepClone (value) { if (!isObject(value)) { return value } }
function isObject(value) { const type = typeof value return value != null && (type === 'object' || type === 'function') }
|
接下来是怎么拷贝引用类型。我会按照以下顺序来介绍:
- 数组
- 函数
- 对象
- 特殊类型。Boolean、Date、Map、Number 等等
另外,lodash 还实现了 Buffer(node.js)等拷贝,但我实际用得不多,就不展开了,有兴趣的可以去看看源码。
2. 拷贝数组
2.1 初始化
先初始化一个长度为原数组长度的数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| export function deepClone(value) { let result
if (!isObject(value)) { return value }
const isArr = Array.isArray(value) if (isArr) { result = initCloneArray(value) } }
const hasOwnProperty = Object.prototype.hasOwnProperty
function initCloneArray(array) { const { length } = array const result = new array.constructor(length)
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) { result.index = array.index result.input = array.input } return result }
|
2.2 赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export function deepClone(value) { let result
if (!isObject(value)) { return value }
const isArr = Array.isArray(value) if (isArr) { result = initCloneArray(value) }
if (isArr) { for (let i = 0; i< value.length; i++) { result[i] = deepClone(value[i]) } }
return result }
|
3. 拷贝函数
函数的拷贝的话,我们还是返回之前的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| export function deepClone(value) { let result
if (!isObject(value)) { return value }
const isArr = Array.isArray(value) if (isArr) { result = initCloneArray(value) } else { const isFunc = typeof value === 'function' if (isFunc) { return value } }
if (isArr) { for (let i = 0; i< value.length; i++) { result[i] = deepClone(value[i]) } }
return result }
|
4. 拷贝对象
初始化一个对象,然后赋值。
要注意的是这个拷贝后的对象和原对象的原型链是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| function deepClone(value) { let result if (!isObject(value)) { return value } const isArr = Array.isArray(value) const tag = getTag(value)
if (isArr) { result = initCloneArray(value) } else { const isFunc = typeof value === 'function' if (isFunc) { return value }
if (tag == '[object Object]' || tag == '[object Arguments]') { result = initCloneObject(value) } }
if (isArr) { for (let i = 0; i< value.length; i++) { result[i] = deepClone(value[i]) } } else { Object.keys(Object(value)).forEach(k => { result[k] = deepClone(value[k]) }) }
return result
}
function getTag(value) { if (value == null) { return value === undefined ? '[object Undefined]' : '[object Null]' } return toString.call(value) }
const objectProto = Object.prototype function isPrototype(value) { const Ctor = value && value.constructor const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto return value === proto }
function initCloneObject(object) { return (typeof object.constructor === 'function' && !isPrototype(object)) ? Object.create(Object.getPrototypeOf(object)) : {} }
|
5. 拷贝特殊对象
包括 Boolean
, Date
, Map
, Number
, RegExp
, Set
, String
, Symbol
接下来的思路也是一样的,先调用对应的构造函数。然后赋值就行了。稍微麻烦一点的可能是 Regexp 正则对象和 Symbol 对象
5.1 初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| function deepClone(value) { let result if (!isObject(value)) { return value }
const isArr = Array.isArray(value) const tag = getTag(value)
if (isArr) { result = initCloneArray(value) } else { const isFunc = typeof value === 'function' if (isFunc) { return value } if (tag == '[object Object]' || tag == '[object Arguments]') { result = initCloneObject(value) } else { result = initCloneByTag(value, tag) } }
if (isArr) { for (let i = 0; i< value.length; i++) { result[i] = deepClone(value[i]) } } else { Object.keys(Object(value)).forEach(k => { result[k] = deepClone(value[k]) }) }
return result
}
const toString = Object.prototype.toString
function getTag(value) { if (value == null) { return value === undefined ? '[object Undefined]' : '[object Null]' } return toString.call(value) }
const objectProto = Object.prototype function isPrototype(value) { const Ctor = value && value.constructor const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto return value === proto }
function initCloneByTag(object, tag, isDeep) { const Ctor = object.constructor switch (tag) { case '[object Boolean]': case '[object Date]': return new Ctor(+object) case '[object Set]': case '[object Map]': return new Ctor case '[object Number]': case '[object String]': return new Ctor(object) case '[object RegExp]': return cloneRegExp(object) case '[object Symbol]': return cloneSymbol(object) } }
const reFlags = /\w*$/
function cloneRegExp(regexp) { const result = new regexp.constructor(regexp.source, reFlags.exec(regexp)) result.lastIndex = regexp.lastIndex return result }
const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) { return Object(symbolValueOf.call(symbol)) }
|
5.2 赋值
虽然是特殊对象,但也是对象,所以我们的思路还是获取该对象的所有属性,然后赋值就可以了。
需要注意的是
Object.keys
不能获取 Symbol 属性,可以再加上 Object.getOwnPropertySymbols()
来获取所有 Symbol 属性名
- Set 和 Map 的赋值是通过 add 和 set 来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| function deepClone(value) { let result if (!isObject(value)) { return value }
const isArr = Array.isArray(value) const tag = getTag(value)
if (isArr) { result = initCloneArray(value) } else { const isFunc = typeof value === 'function' if (isFunc) { return value } if (tag == '[object Object]' || tag == '[object Arguments]') { result = initCloneObject(value) } else { result = initCloneByTag(value, tag) } }
if (tag === mapTag) { value.forEach((subValue, key) => { result.set(key, deepClone(subValue)) }) return result }
if (tag === setTag) { value.forEach((subValue) => { result.add(deepClone(subValue)) }) return result }
if (isArr) { for (let i = 0; i< value.length; i++) { result[i] = deepClone(value[i]) } } else { Object.keys(Object(value)).forEach(k => { result[k] = deepClone(value[k]) })
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable Object.getOwnPropertySymbols(value) .filter((symbol) => propertyIsEnumerable.call(value, symbol)) .forEach(k => { result[k] = deepClone(value[k]) }) }
return result
}
|
6. 测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const set = new Set() set.add('gioia') set.add('me')
const map = new Map() map.set(0, 'zero') map.set(1, 'one')
const original = { name: 'gioia', getName: function () { return this.name }, [Symbol()]: 'symbol prop', sym: Symbol('symbol value'), friends: [{ name: 'xkld', }, 'cln'], dress: { pants: { color: 'black' }, shirts: { colors: ['blue', 'white'] } }, map: map, set: set }
const copy = deepClone(original) console.log(copy) console.log(copy.sym === original.sym) console.log(copy.friends === original.friends) console.log(copy.map === original.map)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { name: 'gioia', getName: [Function: getName], sym: Symbol(symbol value), friends: [ { name: 'xkld' }, 'cln' ], dress: { pants: { color: 'black' }, shirts: { colors: [ 'blue', 'white' ] } }, map: Map { 0 => 'zero', 1 => 'one' }, set: Set { 'gioia', 'me' }, [Symbol()]: 'symbol prop' }
true false false
|
7. 解决循环引用
以上我们已经初步实现了一个深拷贝了。但是在循环引用的场景下,会出现栈溢出的现象。
例如 original.circle = original
这种情况,我们要是还递归地赋值的话,就永远也没有尽头🥱
解决办法就是,看看我们要拷贝的对象之前有没有处理过,有的话就直接引用就行了;没有的话再进行赋值并记录在案。你可以选择很多存储方案,像 Map,只要能记录键值就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| function deepClone(value) { let result if (!isObject(value)) { return value }
const isArr = Array.isArray(value) const tag = getTag(value)
if (isArr) { result = initCloneArray(value) } else { const isFunc = typeof value === 'function' if (isFunc) { return value } if (tag == '[object Object]' || tag == '[object Arguments]') { result = initCloneObject(value) } else { result = initCloneByTag(value, tag) } }
cache || (cache = new Map()) const cached = cache.get(value) if (cached) { return cached } cache.set(value, result)
if (tag === mapTag) { value.forEach((subValue, key) => { result.set(key, deepClone(subValue)) }) return result }
if (tag === setTag) { value.forEach((subValue) => { result.add(deepClone(subValue)) }) return result }
if (isArr) { for (let i = 0; i< value.length; i++) { result[i] = deepClone(value[i]) } } else { Object.keys(Object(value)).forEach(k => { result[k] = deepClone(value[k]) })
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable Object.getOwnPropertySymbols(value) .filter((symbol) => propertyIsEnumerable.call(value, symbol)) .forEach(k => { result[k] = deepClone(value[k]) }) }
return result
}
|