乐趣区

关于前端:教你写一个深拷贝函数

拷贝的意义

所谓拷贝,是克隆 数据,是要在不扭转原数据的状况下的操作数据。

有些文章或面试官提到的拷贝函数、拷贝类,纯正没事找事,拷贝进去的与原性能一样,干嘛不应用原函数

想要扩大函数就用新函数封装,想扩大类就应用继承,拷贝 性能 是齐全无意义的操作

拷贝的分类

拷贝分两种,浅拷贝和深拷贝

浅拷贝

浅拷贝只会开展拷贝对象第一层,如果数据内又蕴含了援用类型,克隆出的对象仍旧指向原对象的援用,批改克隆对象可能会影响到原对象。

个别浅拷贝举荐应用 ... 开展运算符,快捷不便

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 办法来针对性的实现拷贝的性能。

结语

目前没有一款深拷贝函数能完满实现所有需要,本文给出了一个较为通用的深拷贝函数,心愿读者可能了解并把握,在有需要的时候专门定制本人的拷贝函数。

如果文中有不了解或不谨严的中央,欢送评论发问。

如果喜爱或有所帮忙,心愿能点赞关注,激励一下作者。

退出移动版