聊一下如何手写一个简单的深拷贝

lxf2023-04-05 09:46:01

前言

上篇文章我们聊了赋值,深拷贝与浅拷贝 Admin.net/post/721216… ,以及手写深拷贝要考虑的因素等。本文我们自己来简单实现一下深拷贝,了解一下实现过程,这个也是面试中常问的点!

数据类型判断

常用的几种数据类型判断 typeof ,instanceof, constructor 以及 Object.prototype.toString.call 等。我们会在下篇文章详细聊数据类型的判断方式以及各个方式的优缺点。本篇文章我们采用 Object.prototype.toString.call 来进行常用数据类型判断

//判断数据类型
const getValueType= (obj) => Object.prototype.toString.call(obj)

*可遍历类型*
const isArray = '[object Array]'
const isObject = '[object Object]'
const isMap = '[object Map]'
const isSet = '[object Set]'
const isArgements = '[object Arguments]'

// 可遍历数据存入数组,对该类型数据进行递归处理
const isEnumerable = [isArray, isObject, isMap, isSet, isArgements]

*不可遍历类型*
const boolType = '[object Boolean]'
const numType = '[object Number]'
const strType = '[object String]'
const dateType = '[object Date]'
const isReg = '[object RegExp]'
const isSymbol = '[object Symbol]'
const funType = '[object Function]'

自己来实现一个简单的深拷贝

1 对象,数组 以及常用的基本类型入手

实现思路:拿到数据后根据不同类型做不同的处理,定义新的数据进行赋值返回

const initData = {
  age: 888,
  flag: false,
  version: '我是原始数据',
  skill: ['开发', '划水'],
  message: {
    name: 'JS'
  }
}
const mycloneDeep = (obj: any) => {
  let _obj: any = {}
  const valueType = getValuesType(obj)
  switch (valueType) {
    // 对象
    case isObject:
      for (const key in obj) {
        _obj[key] = obj[key]
      }
      break
    // 数组
    case isArray:
      _obj = []
      for (const key in obj) {
        _obj[key] = obj[key]
      }
      break
    // 基本数据类型
    default:
      _obj = obj
  }
  return _obj
}
const demo = mycloneDeep(initData)

demo.version = '我是拷贝数据'
demo.skill.push('摸鱼')
demo.message.name2 = 'React'
console.log(demo)

聊一下如何手写一个简单的深拷贝

上面代码中发现我们修改 version 对源数据无影响,但是我们修改对象和数组源后数据也发生了变化。什么原因呢?因为这两者是引用类型,我们在进行拷贝赋值时,只引用了地址,并未重新开辟空间存放数据。此时只实现了浅拷贝。此时我们需要针对不是基本类型的数据继续调用该函数进行赋值操作,也就是我们所说的递归

*只对数组和对象这块进行递归处理*
// 对象
    case isObject:
      for (const key in obj) {
        _obj[key] = mycloneDeep(obj[key])
      }
      break
    // 数组
    case isArray:
      _obj = []
      for (const key in obj) {
        _obj[key] = mycloneDeep(obj[key])
      }
      break
    // 基本数据类型

2 Set 类型数据

定义: 它类似于数组,但是成员的值都是唯一的,没有重复的值(用set 方式来进行数据去重是我们比较常用的方法)。 es6.ruanyifeng.com/#docs/set-m… 我们来创建 set 数据看一下

 const mySet: any = new Set([22, 44, 22, { name: '妲己' }, ['李白', '钟馗']])

聊一下如何手写一个简单的深拷贝 Set 原型上常用的方法
add(添加) has(查询) delete(删除) clear(清空) size(长度)
我们继续来添加 Set 类型数据处理

  //Set 数据
     case isSet:
       _obj = new Set()
       for (const keys of obj.keys()) {
         _obj.add(mycloneDeep(keys))
       }
     break

3 Map 类型数据

定义:它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。同样我们来定义一个 Map 类型数据,了解一下它原型上的方法

const myMap: any = new Map([
  ['401', '无权限'],
  ['404', '找不到']
])

聊一下如何手写一个简单的深拷贝

Map 原型上常用的方法
set(添加) get(查询) delete(删除) clear(清空) size(长度)等

// Map 数据
     case isMap:
       _obj = new Map()
       for (const [keys, value] of obj.entries()) {
         _obj.set(keys, mycloneDeep(value))
       }
     break

4 正则

正则是我们在日常开发中比较常用的,(但我个人很少写正则,都是直接拿工具匹配,对具体的使用细节不是很了解,后面需要补一下知识点了)。正则的两种定义方式 一种是字面量,一种是通过 new 我们来简单看下

const reg1 = /123/g
const reg2 = new RegExp(/123/, 'g')
const {source, flags} = reg1; 

当我们接收一个正则时,我们希望用 new 方式来进行创建,这里面有几个属性需要我们提取出来文本 (通过source属性访问),修饰符(通过flags属性访问)还有一个 lastIndex 属性(用于指定下次匹配从什么时候开始)

聊一下如何手写一个简单的深拷贝

而这个 lastIndex 是可以被随意改的,我们简单看下一个例子,赋值与不赋值得到的结果还不一致,所以这个我们也需要处理一下

聊一下如何手写一个简单的深拷贝

添加正则类型判断

case isReg:
      const { resource, flags, lastIndex } = obj
      _obj = new RegExp(resource, flags)
      _obj.lastIndex = lastIndex
    break

5 Arguments 对象

我们常见 Arguments 对象会出现在函数参数里面,或者获取一个dom对象,

//我们可以通过函数方式来创建一个 arguments 对象
const getArguments = function () {
  return arguments
} //不能用箭头函数,箭头函数无 arguments 对象
case isArgements:
      _obj = getArguments()
      for (const keys in obj) {
        _obj[keys] = mycloneDeep(obj[keys])
      }
break

6 函数

一般针对函数的拷贝,我们都是原封不动的返回,也没遇到要拷贝函数的场景。查了一些资料,我们只做简单的了解

第一种 使用正则

思路与正则一样,把函数转为字符串,然后正则匹配函数的参数,函数体,最后通过new Function()的形式创建一个新函数,但考虑到函数柯里化,闭包,箭头函数,递归调用等等,做起来还是蛮复杂的!

第二种 借用eval
const foo = x => console.log(x + 1);
const fn = eval(foo.toString())
fn(1);// 2

通过toString将函数转为字符串后,借用eval执行再次得到函数,看似可以,但只要函数是函数声明,这种做法就完全行不通了:

function foo(x) {
  console.log(x + 1);
};
console.log(foo.toString())
const fn = eval(foo.toString());
fn(1);// 报错,fn是undefined

如果面试时聊到拷贝函数时说下大概思路就可以了,他也不会揪着不放这个没有特别实际意义的问题。

7 循环引用

// 我们加一行代码 
// 因为是引用类型,然后相互引用,成了无敌洞直接爆炸
 initData.initData = initData
 const demo = mycloneDeep(initData)

聊一下如何手写一个简单的深拷贝

我们可以想想被拷贝的原对象是怎么诞生的,将对象的key指向自己即可。也就是在拷贝时,我们只要保证执行一次obj['obj'] = obj即可,只要让自己指向自己,这个循环引用自然就会诞生,并不需要我们无限递归来模拟这个循环引用。

怎么跳出这个递归呢?设想下,obj在第一次传入后,开始第一次递归,然后把自己又作为参数传了下去,后续做的事情完全是相同的,那我们是不是可以记录我们要拷贝的obj以及它拷贝的后的结果,当下次遇到相同的obj跳出递归,直接返回之前的结果就好了。

考虑到我们要记录的参数可能是对象类型,使用普通的对象肯定不行,而es6新增的Map数据类型key就可以使用对象

代码实现

我们来整理一下代码,顺便对我们之前的代码进行一下优化!

<script lang="ts">
// 判断数据类型
const getValuesType: any = (value: any) => Object.prototype.toString.call(value)
// 可遍历
const isArray = '[object Array]'
const isObject = '[object Object]'
const isMap = '[object Map]'
const isSet = '[object Set]'
const isArgements = '[object Arguments]'
// 可遍历数据存入数组,对改数据进行递归处理
const isEnumerable = [isArray, isObject, isMap, isSet, isArgements]

// 不可遍历
// const boolType = '[object Boolean]'
// const numType = '[object Number]'
// const strType = '[object String]'
// const dateType = '[object Date]'
// const funType = '[object Function]'
const isReg = '[object Regexp]'
const isSymbol = '[object Symbol]'

const mycloneDeep: any = (obj: any, map = new Map()) => {
  const types = getValuesType(obj)
  let _obj: any = {}
  if (isEnumerable.includes(types)) { 
// 可遍历类型数据
    _obj = new obj.constructor()
// 它能根据任意对象类型创建对应空对象,比如类型是对象,会创建一个{},如果是数组,会创建一[]。(javascript中的原型知识点,我们后文会聊)
    
    // 解决循环引用问题
    if (map.has(obj)) {
      return map.get(obj)
    }
    // 存储当前拷贝的对象,以及我们要返回的对象
    map.set(obj, _obj)
    
    switch (types) {
      // 数组 对象 Argements
      case isArray:
      case isObject:
      case isArgements:
        for (const key in obj) {
          _obj[key] = mycloneDeep(obj[key], map)
        }
        break
      // Set
      case isSet:
        for (const keys of obj.keys()) {
          _obj.add(mycloneDeep(keys), map)
        }
        break
      // Map
      case isMap:
        for (const [keys, value] of obj.entries()) {
          _obj.set(keys, mycloneDeep(value), map)
        }
        break
      default:
        _obj = obj
    }
  } else {
    // 不可遍历
    switch (types) {
      // 正则
      case isReg:
        const { resource, flags, lastIndex } = obj
        _obj = new RegExp(resource, flags)
        _obj.lastIndex = lastIndex
        break
      // Symbol
      case isSymbol:
        _obj = Object(obj.valueOf()) 
        // 一般情况下我们让它跟普通的数字一样,传入原封不动返回 如 Symbol(1) //  但我们需要额外考虑包装对象形式的Symbol 如 Symbol({name:'测试'})
        break
      default:
        _obj = obj
    }
  }
  return _obj
}
</script>

最后

这样我们就实现了一个简单的深拷贝,针对不同类型一个个来处理,还是比较比较好理解和实现的。我们实际项目开发中会直接使用封装好的库如 lodash ,我们自己写的只是做一个知识点的学习,知道如何实现即可。 这篇文章就到这里了,我们下篇文章再见!