深拷贝
题目
手写 JS 深拷贝
分析
这是一个很常见的问题,看似也很简单,但是如果考虑到“高质量代码”的要求,写起来还是挺麻烦的。
别说写代码,就本节所有的情况你能否考虑全面,这都不一定。
typeof 运算符
- 识别所有值类型
- 识别函数
- 判断是否是引用类型(不可再细分)
- typeof null === 'object'
JSON.stringfy() 实现深拷贝存在的问题:
- 执行会报错:存在BigInt类型、循环引用。
- 拷贝Date引用类型会变成字符串。
- 键值会消失:对象的值中为Function、Undefined、Symbol 这几种类型,。
- 键值变成空对象:对象的值中为Map、Set、RegExp这几种类型。
- 无法拷贝:不可枚举属性、对象的原型链。
- 补充:其他更详细的内容请查看官方文档:JSON.stringify() - JavaScript | MDN (mozilla.org)
深拷贝需要考虑的问题:
- 基本类型数据是否能拷贝?
- 键和值都是基本类型的普通对象是否能拷贝?
- Symbol作为对象的key是否能拷贝?
- Date和RegExp对象类型是否能拷贝?
- Map和Set对象类型是否能拷贝?
- Function对象类型是否能拷贝?(函数我们一般不用深拷贝)
- 对象的原型是否能拷贝?
- 不可枚举属性是否能拷贝?
- 循环引用是否能拷贝?
错误答案1
使用 JSON.stringify
和 JSON.parse
- 无法转换函数
- 无法转换
Map
Set
- 无法转换循环引用
PS:其实普通对象使用 JSON API 的运算速度很快,但功能不全
错误答案2
使用 Object.assign
—— 这根本就不是深拷贝,是浅拷贝 !!!
错误答案3
只考虑了普通的对象和数组
- 无法转换
Map
Set
- 无法转换循环引用
/**
* 深拷贝 - 只考虑了简单的数组、对象
* @param obj obj
*/
function cloneDeep(obj: any) {
if (typeof obj !== 'object' || obj == null ) return obj
let result: any
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = cloneDeep(obj[key]) // 递归调用
}
}
return result
}
// 功能测试
const a: any = {
set: new Set([10, 20, 30]),
map: new Map([['x', 10], ['y', 20]])
}
a.self = a
console.log( cloneDeep(a) ) // 无法处理 Map Set 和循环引用
正确答案
循环引用 Map Set 函数
for...in
语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。
/**
* 深拷贝
* @param obj obj
* @param map weakmap 为了避免循环引用
*/
export function cloneDeep(obj: any, map = new WeakMap()): any {
if (typeof obj !== 'object' || obj == null ) return obj
// 避免循环引用
const objFromMap = map.get(obj)
if (objFromMap) return objFromMap
let target: any = {}
map.set(obj, target)
// Map
if (obj instanceof Map) {
target = new Map()
obj.forEach((v, k) => {
const v1 = cloneDeep(v, map)
const k1 = cloneDeep(k, map)
target.set(k1, v1)
})
}
// Set
if (obj instanceof Set) {
target = new Set()
obj.forEach(v => {
const v1 = cloneDeep(v, map)
target.add(v1)
})
}
// Array
if (obj instanceof Array) {
target = obj.map(item => cloneDeep(item, map))
}
// Object
for (const key in obj) {
const val = obj[key]
const val1 = cloneDeep(val, map)
target[key] = val1
}
return target
}
// // 功能测试
// const a: any = {
// set: new Set([10, 20, 30]),
// map: new Map([['x', 10], ['y', 20]]),
// info: {
// city: '北京'
// },
// fn: () => { console.info(100) }
// }
// a.self = a
// console.log( cloneDeep(a) )
最终完整版实现:
/**
* 深拷贝完整版实现
* @param {Object} target
* @returns
*/
function deepClone(target = {}) {
// WeakMap作为记录对象的hash表 (用于防止循环引用)
const map = new WeakMap()
// 工具函数
function isObject(target) {
return (typeof target === 'object') || typeof target === 'function'
}
// 拷贝主逻辑
function clone(data) {
// 基础类型直接返回
if (!isObject(data)) {
return data
}
// 日期或者正则对象,则直接构造一个新的对象返回
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data)
}
// 处理函数对象
if (typeof data === 'function') {
return new Function('retuen' + data.toString())()
}
// 如果对象已经存在,则直接返回该对象
const exist = map.get(data)
if (exist) {
return exist
}
// 处理 Map 对象
if (data instanceof Map) {
const result = new Map()
map.set(data, result)
data.forEach((val, key) => {
// 注意:map种的值为 object 的话也得深拷贝
if (isObject(val)) {
result.set(key.clone(val))
} else {
result.set(key, val)
}
})
return result
}
// 处理 Set 对象
if (data instanceof Set) {
const res = new Set()
map.set(data, res)
data.forEach(val => {
if (isObject(val)) {
res.add(clone(val))
} else {
res.add(val)
}
})
return res
}
// 收集键名(考虑以Symbol作为key以及不可枚举的属性)
// Reflect.ownKeys(obj)相当于[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]
const keys = Reflect.ownKeys(data)
// 获得对象的所有属性以及对应的属性描述
const allDesc = Object.getOwnPropertyDescriptors(data)
// 创建一个新对象, 继承传入对象的原型链、(浅拷贝)
const result = Object.create(Object.getPrototypeOf(data), allDesc)
// 新对象加入map中,进行记录
map.set(data, result)
// Object.create() 是浅拷贝,所以要判断值的类型并递归进行深拷贝
keys.forEach(key => {
const val = data[key]
if (isObject(key)) {
result[key] = clone(val)
} else {
result[key] = val
}
})
return result
}
return clone(data)
}