共计 3620 个字符,预计需要花费 10 分钟才能阅读完成。
拷贝的意义
所谓拷贝,是克隆 数据,是要在不扭转原数据的状况下的操作数据。
有些文章或面试官提到的拷贝函数、拷贝类,纯正没事找事,拷贝进去的与原性能一样,干嘛不应用原函数
想要扩大函数就用新函数封装,想扩大类就应用继承,拷贝 性能 是齐全无意义的操作
拷贝的分类
拷贝分两种,浅拷贝和深拷贝
浅拷贝
浅拷贝只会开展拷贝对象第一层,如果数据内又蕴含了援用类型,克隆出的对象仍旧指向原对象的援用,批改克隆对象可能会影响到原对象。
个别浅拷贝举荐应用 ...
开展运算符,快捷不便
const arr = [1, 2, 3]
const arrClone = [...arr]
const obj = {
a: 1,
b: {c: 2,},
}
const objClone = {...obj,}
objClone.a = 2
objClone.b.c = 3
console.log(obj.a) // 1
console.log(obj.b.c) // 3
深拷贝
在上一节浅拷贝曾经发现问题了,在拷贝多层援用对象后,批改克隆对象时原对象数据可能也会跟着变,这显著是咱们不心愿的。
深拷贝就是要解决这个问题,对于多层的数据,逐层拷贝
最常见的深拷贝是借助 JSON 转换:JSON.parse(JSON.stringify(obj))
但 JSON 转换存在很多有余
- JSON 只能转换一般对象和数组,JS 中许多类对象并不反对,比方:Map、Set、Date、RegExp 等等
- JSON 在转换某些根底类型也存在问题,比方:NaN 转换成 null、疏忽 Symbol、BigInt 报错
-
JSON 无奈解决循环援用的问题
const obj = {} obj.obj = obj JSON.stringify(obj) // TypeError: Converting circular structure to JSON
综上,在下一章咱们要实现本人的深拷贝函数
深拷贝实现
代码
先上代码,而后再解说
/**
* @description: 深拷贝函数
* @param {any} value 要拷贝的数据
* @param {Map} [stack] 记录已拷贝的对象,防止循环援用
* @return {any} 拷贝实现的数据
*/
function deepClone(value, stack) {const objectTag = '[object Object]'
const setTag = '[object Set]'
const mapTag = '[object Map]'
const arrayTag = '[object Array]'
// 获取对象类标签
const tag = Object.prototype.toString.call(value)
// 只须要递归深拷贝的品种有 对象、数组、汇合、映射
// 其余一律间接返回
const needCloneTag = [objectTag, arrayTag, setTag, mapTag]
if (!needCloneTag.includes(tag)) {return value}
// 无奈获取代理对象的属性名,只能返回
if (value instanceof Proxy) {return value}
// 返回的后果继承原型
let result
if (tag == arrayTag) {
// 因为 Array 的空属性不会被遍历,单纯继承原型会导致长度不一
result = new value['__proto__'].constructor(value.length)
} else {result = new value['__proto__'].constructor()}
// 记录已拷贝的对象
// 用于解决循环援用的问题
stack || (stack = new Map())
if (stack.has(value)) {return stack.get(value)
}
stack.set(value, result)
// 递归拷贝映射
if (tag == mapTag) {for (const [key, item] of value) {result.set(key, deepClone(item, stack))
}
}
// 递归拷贝汇合
if (tag == setTag) {for (const item of value) {result.add(deepClone(item, stack))
}
}
// 递归拷贝对象 / 数组的属性
for (const prop of Object.keys(value)) {result[prop] = deepClone(value, stack)
}
// 拷贝符号属性
for (const sy of Object.getOwnPropertySymbols(value)) {result[sy] = deepClone(value, stack)
}
return result
}
解说
在下面的代码中咱们是依据传入数据的 类标签 来辨别数据类型的,类标签 相干内容能够查看 细述 JS 各数据类型的检测与转换 或 symbol 类型用法介绍
对于要递归深拷贝的对象,在此阐明一下:
- 咱们只用递归深拷贝 存有数据 的对象:对象、数组、汇合、映射。
- 对于根底数据类型,无奈存储数据,间接返回。
- 对于 Date、RegExp、Function、Number、String 等对象,因为它们的属性均是不可扭转的,应用原对象与克隆对象性能雷同,也无需拷贝,同样间接返回。
- 对于无奈遍历的对象或属性,比方:弱援用对象(WeakMap WeakSet)、代理对象(Proxy)、应用
Object.defineProperty
定义的不可迭代属性,因为无奈获取它们的键 / 属性,也就无奈拷贝。 - 还有一些类数组对象也能存储数据(Typed Arrays、ArrayBuffer、arguments、nodeList),它们在平时应用的并不多,而且拷贝形式也与数组相似,为了简便没有在代码中体现。
下一步,调用对象原型的结构器获取新示例同时也继承原型,因为复制的数组要与原数组长度雷同,所以调用数组 (或其子类) 的构造函数时要传入长度。
而后通过一个 Map 记录原对象中曾经拷贝过的对象,防止循环援用有限递归的问题
最初依据对象的类型,递归拷贝其属性值,对 Map 和 Set 特地解决,对象和数组都能够通过 Object.keys()
获取所有键 / 索引,再拷贝一遍符号属性,完结深拷拷贝代码。
总结
咱们本人实现的深拷贝函数,比照 JSON 转换,多了以下长处
- 可能解决 Map、Set 等数据类型
- 可能继承原型的属性
- 解决了循环援用的问题
尽管咱们的深拷贝代码能够复制类的实例,但对于构造函数会产生副作用的类,可能会呈现谬误
上面是我在我的项目中遇到的一个 Bug
const globalData = {project: null,}
class Project {constructor() {
this.itemId = 0 // 用于自增的 id
this.itemMap = new Map()}
newItem(item) {this.itemMap.set(++this.itemId, item)
return this.itemId
}
}
class Item {constructor() {
// 每个新建的 Item 都从全局 Project 获取 Id,并退出到 itemMap 中
this.itemId = globalData.project.newItem(this)
}
}
const project = new Project()
globalData.project = project
const item = new Item()
console.log(globalData.project)
// Project {
// itemId:1
// itemMap: Map(1) {1 => Item}
// }
const clone = deepClone(project) // 有限创立 Item,页面卡死
探索起因就是因为 for of
遍历 itemMap 时,创立了新的 Item 增加进 itemMap 中,新的 Item 又被迭代,导致了有限创立、增加 Item
解决办法也有,就是将要遍历的属性先保留到数组中,只遍历数组
// 递归拷贝映射
if (tag == mapTag) {for (const key of [...value.keys()]) {result.set(key, deepClone(value.get(key), stack))
}
}
// 递归拷贝汇合
if (tag == setTag) {for (const item of [...value.values()]) {result.add(deepClone(item, stack))
}
}
但这不肯定合乎咱们想要的后果,比方咱们不心愿新克隆的对象被退出到 itemMap 中
所以我在我的项目中,为那些构造函数会产生副作用的类定义了本人的 clone
办法来针对性的实现拷贝的性能。
结语
目前没有一款深拷贝函数能完满实现所有需要,本文给出了一个较为通用的深拷贝函数,心愿读者可能了解并把握,在有需要的时候专门定制本人的拷贝函数。
如果文中有不了解或不谨严的中央,欢送评论发问。
如果喜爱或有所帮忙,心愿能点赞关注,激励一下作者。