JS这门语言没有提供深拷贝的内建方法,诸如slice, concat, Object.assign
这些方法其实都是对象的浅拷贝,修改深层次引用时就会变更原始数据,这在一些必须使用深拷贝的场景是无法接受的,所以如何实现一个有用又靠谱的深拷贝方法变得至关重要。
如果不想搞得太复杂,可以直接使用这个懒汉大法:JSON.parse(JSON.stringify(target))
,它的思路很简单,先序列化再反序列化,得到一个全新的对象。而事实上,在只包含原始数据类型和对象的数据结构中,这种大法是完全可用的。它的主要问题在于:
- 无法保证复制后的同一性
- 无法处理循环引用
具体的问题可以查看JSON.stringify的文档,就不赘述了。
要手动实现一个深拷贝,则需要对症下药——根据不同的数据类型选择合适的拷贝方案,所以首先我们需要判别数据类型:
const TYPE_PRIMITIVE = 1
const TYPE_UNDEF = 2
const TYPE_SYMBOL = 3
const TYPE_DATE = 4
const TYPE_REGEXP = 5
const TYPE_FUNCTION = 6
const TYPE_ARRAY = 7
const TYPE_PLAIN_OBJECT = 8
const TYPE_UNKNOWN = 9
function getType (target) {
let type = Object.prototype.toString.call(target).slice(8, -1)
let innerType = typeof target
if (['number', 'string', 'boolean'].includes(innerType)) {
return TYPE_PRIMITIVE
}
else if (target === null || target === undefined) {
return TYPE_UNDEF
}
else if (type === 'Date') {
return TYPE_DATE
}
else if (innerType === 'symbol') {
return TYPE_SYMBOL
}
else if (type === 'RegExp' && 'lastIndex' in target) {
return TYPE_REGEXP
}
else if (Array.isArray(target)) {
return TYPE_ARRAY
}
else if (innerType === 'function') {
return TYPE_FUNCTION
}
else if (type === 'Object') {
return TYPE_PLAIN_OBJECT
}
else {
return TYPE_UNKNOWN
}
}
我们针对上述数据类型进行拷贝分析:
- 原始数据类型(
Number, String, Boolean
)或者未定义(null, undefined
)则直接返回它就好; Date
,先获取它的时间戳,然后依此创建一个新的Date
对象即可;- 正则表达式,要分别获取它的源(
source
)和flags
,然后依此创建新的正则式; - 内建函数,直接返回它;
- 不是内建函数,则创建一个相同的函数并返回;
Symbol
,返回一个与它描述相同的Symbol即可- 对象和数组,需要遍历地拷贝它们的每个属性或索引,而这些属性有可能也是个对象或者数组,所以必须递归地执行拷贝。另外一个问题是循环引用,举个例子
a['x'] = b; b['y'] = a
,如果不加限制,copy(a)
的递归会一直进行下去直到栈溢出。解决方法是用一个weakMap
保存[原始对象]: [拷贝对象]
的映射关系,按上面的例子来说就是,如果执行copy(a)
, weakMap会依次添加a: copy_a
,b: copy_b
的映射,然后递归子例程copy(b)
会继续递归执行copy(a)
,此时映射关系中存在a
的映射,所以直接返回a
对应的拷贝对象即可,这样就消除了无限递归。
具体实现:
function isNative (constructor) {
return typeof constructor === 'function' && /native code/.test(constructor.toString())
}
function copyRegExp(regExp) {
const flags = /\w+/.exec(regExp.flags)
const result = new regExp.constructor(regExp.source, flags ? flags[0] : '')
result.lastIndex = regExp.lastIndex
return result
}
function copySymbol(symbol) {
let description = symbol.description || symbol.toString().slice(7, -1)
return Symbol(description)
}
function copyFunction(func) {
if (isNative(func)) return func
let f = new Function(`return ${func}`)
return f()
}
function copyProto(orig, dist) {
let _proto = Object.getPrototypeOf(orig)
Object.setPrototypeOf(dist, _proto)
return dist
}
export default function copy(target, withProto, hMap) {
let type = getType(target)
switch (type) {
case TYPE_PRIMITIVE:
case TYPE_UNDEF:
return target
case TYPE_DATE:
return new Date(target.getTime())
case TYPE_REGEXP:
return copyRegExp(target)
case TYPE_FUNCTION:
return copyFunction(target)
case TYPE_SYMBOL:
return copySymbol(target)
case TYPE_ARRAY:
// ignore properties in array
hMap = hMap || new WeakMap()
if (hMap.has(target)) {
return hMap.get(target)
}
else {
let mirList = []
hMap.set(target, mirList)
for (let i = 0; i < target.length; i++) {
mirList.push(copy(target[i], withProto, hMap))
}
return withProto ? copyProto(target, mirList) : mirList
}
case TYPE_PLAIN_OBJECT:
hMap = hMap || new WeakMap()
if (hMap.has(target)) {
return hMap.get(target)
} else {
let mirObj = {}
hMap.set(target, mirObj)
// ignore properties not enumerable
// ignore Symbol keys
let keys = Object.keys(target)
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
mirObj[key] = copy(target[key], withProto, hMap)
}
return withProto ? copyProto(target, mirObj) : mirObj
}
default:
console.warn('can\'t detect type of the target: ' + target)
return target
}
}