1. 区分数据类型
JavaScript 有 8 种数据类型:string number bool null undefined symbol bigint object。
在拷贝数据时,要判断数据类型,object 的情况较多需单独处理,其它基本数据类型可直接返回。
function deepClone(target) {
if (typeof target === 'object') {
let result = {}
for (let key in target) {
result[key] = deepClone(target[key])
}
return result
} else {
return target
}
};
2. 兼容数组
考虑数组的情况,改写如下:
function deepClone(target) {
if (typeof target === 'object') {
let result = Array.isArray(target) ? [] : {} //区分数组和对象
for (let key in target) {
result[key] = deepClone(target[key])
}
return result
} else {
return target
}
}
function cloneOtherType(target, type) {
const Ctor = target.constructor
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target)
case regexpTag:
return cloneReg(target)
case symbolTag:
return cloneSymbol(target)
default:
return null
}
}
拷贝正则和 Symbol 类型的方法:
function cloneReg(target) {
const reFlags = /\w*$/
const result = new targe.constructor(targe.source, reFlags.exec(targe))
result.lastIndex = targe.lastIndex
return result
}
function cloneSymbol(target) {
return Object(Symbol.prototype.valueOf.call(target))
}
还有其它类型不一一处理,重点是清楚实现深拷贝的整体思路。
改写后的深拷贝为:
function deepClone(target, cache) {
// 直接返回原始数据类型
if(!isObject(target)) {
return target
}
const type = getType(target)
let result
// 初始化对应类型
if(deepTag.includes(type)) {
result = getInit(target, type)
} else {
return cloneOtherType(target, type)
}
if(!cache) {
cache = new Map() //如果没有缓存,则创建
}
// 放置循环引用
if (cache.get(target)) {
return cache.get(target)
}
cache.set(target, result)
// 拷贝 set
if (type === setTag) {
target.forEach(value => {
result.add(deepClone(value,cache))
});
return result
}
// 拷贝 map
if (type === mapTag) {
target.forEach((value, key) => {
result.set(key, deepClone(value,cache))
});
return result
}
// 拷贝对象和数组
let index = -1
let keys = isArray ? undefined : Object.keys(target)
let array = keys || target
const length = keys ? keys.length : target.length
while (++index < length) {
let key = index
let value = array[index]
if (keys) {
key = value
}
result[key] = deepClone(target[key], cache)
}
return result
}
另一种较为简洁的深拷贝写法,适用于常用的部分类型
const deepClone = (target, cache) => {
if(!cache){
cache = new Map() // 缓存不能全局,最好临时创建并递归传递
}
if(target instanceof Object) {
if(cache.get(target)) {
return cache.get(target)
}
let result
if(target instanceof Function) {
if(target.prototype) { // 有 prototype 就是普通函数
result = function(){ return target.apply(this, arguments) }
} else {
result = (...args) => { return target.call(undefined, ...args) }
}
} else if(target instanceof Array) {
result = []
} else if(target instanceof Date) {
result = new Date(target - 0)
} else if(target instanceof RegExp) {
result = new RegExp(target.source, target.flags)
} else {
result = {}
}
cache.set(target, result)
for(let key in target) {
if(target.hasOwnProperty(key)){
result[key] = deepClone(target[key], cache)
}
}
return result
} else {
return target
}
}
对象的引用和复制
赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址”,即对该对象的“引用”。当我们对对象执行操作时,例如获取一个属性,JavaScript 引擎会查看该地址中的内容,并在实际对象上执行操作。 当一个对象变量被复制时,只是该引用被复制,而该对象自身并没有被复制。
这时有了两个变量,它们保存的都是对同一个对象的引用。 (图 copy-object)
仅当两个变量都引用同一个对象,它们才相等。
克隆与合并,Object.assign(浅拷贝)
创建一个新对象,通过遍历已有对象的属性,并在原始类型值的层面复制它们,以实现对已有对象结构的复制。
或者用语法 Object.assign(dest, [src1, src2, src3...]) 代替上述 for 循环。
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
Object.assign 方法只会拷贝源对象可枚举的和自身的属性到目标对象。如果目标对象与源对象具有相同的属性,则目标对象中的属性将被源对象中的属性覆盖。
深层克隆(深拷贝)
而当属性不为原始类型,而是对象时,用 Object.assign() 方法克隆对象则是以引用形式被拷贝。 为了能复制出两个独立的对象,应该使用一个拷贝循环来检查源对象的每个值,如果它是一个对象,则复制它的结构。这就是所谓的“深拷贝”。 可以用递归方式实现深拷贝,或者采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)。
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
实现深拷贝 方法1:JSON
不建议使用该方法,该方法有如下缺点:
实现深拷贝 方法2:递归
1. 区分数据类型 JavaScript 有 8 种数据类型:string number bool null undefined symbol bigint object。 在拷贝数据时,要判断数据类型,object 的情况较多需单独处理,其它基本数据类型可直接返回。
2. 兼容数组 考虑数组的情况,改写如下:
3. 解决循环引用 如果使 target.self = target,在拷贝时,则会出现循环引用,导致死循环。 可以用 Map 来存储当前对象和拷贝对象的对应关系,如果已经拷贝过当前对象,则直接返回,如果没有,则继续拷贝。
然而 Map 会形成对于对象的引用,而不再需要所引用的对象时,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放所引用的对象占用的内存,而造成内存泄漏。 为了解决这个问题,ES6 新增了 WeakMap 数据结构。 WeakMap与Map的区别有两点:
4. 性能优化,用 while 替代 for...in 经过测试可知,while 的速度比 for...in 快,用 while 可提升代码效率。
5. 处理其它数据类型 用 typeof 判断类型的返回值中,null 类型的结果是 "object",function 对象的结果是 function,判断对象是否为引用类型时要区分这两种情况。
然后用 toString() 方法获取准确的引用类型:
这些类型里又可以分为两类:
可以继续遍历的类型 由于类型较多,选取部分进行处理,比如 object,array,Map,Set 都属于可以继续遍历类型。 首先需要获取它们的初始化数据,通过该对象的 constructor 来获取。这种方法的好处是:使用原对象的构造方法,可以保留对象原型上的数据。
处理可继续遍历类型:
不可以继续遍历的类型 其他剩余的类型可以统一归类成不可处理的数据类型。 Bool、Number、String、String、Date、Error 这几种类型可以直接用构造函数和原始数据创建一个新对象:
拷贝正则和 Symbol 类型的方法:
还有其它类型不一一处理,重点是清楚实现深拷贝的整体思路。 改写后的深拷贝为:
另一种较为简洁的深拷贝写法,适用于常用的部分类型
笔记参考《现代 JavaScript 教程》,如何写出一个惊艳面试官的深拷贝?,阮一峰 ES6 教程,JavaScript 内存泄漏教程