[基础]js实现深拷贝

1,092 阅读4分钟

这个问题涉及JS的数据类型、数据存储、内存管理。还涉及很多边界条件的考虑,很具有代表性。很好查漏补缺

内存管理

每一个数据存储都都有一个内存空间,内存空间又被分为两种:栈内存(stock)、堆内存(heap)。js会根据数据类型存储在对应内存上

基础数据类型和栈内存

基础数据类型:Number String Null Undefined Boolean Symbol

基础数据类型储存在栈内存,可以直接操作保存在栈内存的值,这个值通过变量来访问。

但是不能修改已经储存的值,比如可以往栈内存中存储了一个数字2,但是这个2不能改为3。

引用数据类型与堆内存

引用数据类型:Array Object Function

引用类型储存在堆内存,栈内存中会存储指向这个堆内存的引用。js引用类型的值大小不固定,可以在不声明长度的情况下静态补充。

js不允许直接访问堆内存的位置,因此不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作栈内存的引用而不是实际的对象。

拷贝

js中基础类型和引用类型的特点不同,所以拷贝时使用的方式也不同

浅拷贝

只拷贝一层数据,是实现深拷贝的基础

基础数据类型

利用栈内存的值不能修改,基础数据类型可以通过直接赋值的方式实现拷贝,不必担心原数据改变引起拷贝后的值发生变化

数组

  • let arr2 = [...arr1]
  • let arr2 = arr1.slice(0)
  • ...

骚操作-拷贝对象

一般拷贝对象会先声明一个变量等于一个空对象let newObj = {},这么做可能会丢失原型链。使用let newObj = new obj.constructor可以在一定程度上保留原型链,还可以使用在拷贝数组上

  • 可以使用的对象
    • 数组
    • 对象
    • 类的实例
let newObj = new obj.constructor // 找出实例
for (let key in obj) {
  if (obj.hasOwnProperty(key)) { // 忽略继承属性
    newObj[key] = obj[key]
  }
}

函数拷贝

核心的两个方法就是 Function.prototype.toStringnew Function()

Function.prototype.toString 可以返回整个方法字符串,如 'function fn(a, b) { console.log(a, b) }'

new Function可以创建新函数,例如创建上面的函数 new Function('a, b', 'console.log(a, b)')

链接这两个方法就需要一个正则分别匹配参数名和函数内容

// 例子
function fn(a, b){ console.log(a, b) }
const reg = /^function \S*\s*\(([\s\S]*)\)\s*{([\s\S]*)}$/

// 第一步:将函数转为字符串
const str = fn.toString() // 'function fn(a, b){ console.log(a, b) }'

// 第二步:拿到参数名与函数内容
const m = str.match(reg) // ["function fn(a, b){ console.log(a, b) }", "a, b", " console.log(a, b) ", index: 0, input: "function fn(a, b){ console.log(a, b) }", groups: undefined]

// 第三步:创建函数
const newFn = new Function(m[1], m[2])

注:正则在这个例子中只是演示,不保证实际工作使用是否会出问题 =。=

拷贝正则、日期对象

构造函数可以直接接受一个正则、日期对象来创建一个新的正则、日期对象

let reg = /^n+$/
let date = new Date()

let newReg = new RegExp(reg)
let newDate = new Date(date)

正则对象中存有 lastIndex,考虑 lastIndex 的话需要 newReg.lastInde === reg.lastInde

Set、Map

构造函数本身就可以接受set、map数据类型,返回新的set、map

var set = new Set([1, 2, 3, 4])
var map = new map(['a', 1], ['b', 2])

var newSet = new Set(set)
var newMap = new Map(map)

WeakSet 、 WeakMap 都不接受自身的数据类型,本身也不支持遍历,暂没有找到拷贝的方法。

symbol

Symbol 只接受字符串,需要取到创建 Symbol 的字符串

let symbol = Symbol(1)

let str = Symbol.prototype.toString.call(symbol) // "Symbol(1)"
str = str.replace(/^Symbol\((\S+)\)$/, '$1')  // "1"
let newSymbol = Symbol(str)

深拷贝

多层拷贝

JSON

let obj = {a: 1, b: {c: 2}}
JSON.parse(JSON.stringify(obj))

缺点:不能含有 undefined、function、正则、日期类型...

深度拷贝函数

使用 WeakMap 解决循环引用的问题

// 类型检测
function _type(value) {
  return Object.prototype.toString.call(value)
}

// 深拷贝
function _deepClone(obj, hash = new WeakMap) {
  if (obj === null) return null
  if (typeof obj !== 'function') return obj // 函数深拷贝没有意义
  if (typeof obj !== 'object') return obj

  if (_type(obj) === '[object RegExp]' || _type(obj) === '[object Date]') {
    return new obj.constructoe(obj)
  }

  let v = hash.get(obj);
  if (v) return v // 如果映射表中有,就是循环引用。直接返回拷贝后的结果

  let newObj = new obj.constructoe
  hash.set(obj, newObj); // 将拷贝前的和拷贝后的做一个映射表
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = _deepClone(obj[key])
    }
  }
  return newObj
}

修改一下支持 Set、Map

// 类型检测
function _type(value) {
  return Object.prototype.toString.call(value).replace(/^\[object (\S+)\]$/g, '$1')
}

// 深拷贝
function _deepClone(obj, hash = new WeakMap) {
  if (obj === null) return null
  if (typeof obj !== 'function') return obj // 函数深拷贝没有意义
  if (typeof obj !== 'object') return obj
  
  if (['RegExp', 'Date', 'Set', 'Map'].includes(_type(obj))) {
    return new obj.constructoe(obj)
  }

  let v = hash.get(obj);
  if (v) return v // 如果映射表中有,就是循环引用。直接返回拷贝后的结果

  let newObj = new obj.constructoe
  hash.set(obj, newObj); // 将拷贝前的和拷贝后的做一个映射表
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = _deepClone(obj[key])
    }
  }
  return newObj
}