面试官系列:请说说你对深拷贝、浅拷贝的理解

746 阅读4分钟

前言

本文手写的cloneDeep 函数作者已经将其用于实际生产项目中,在生产运行了两年多时间没有发现任何问题,大家可放心食用。 纸上得来终觉浅,要知此事须躬行。各位coder,请随我一起看下去(ง •_•)ง。

浅拷贝

对于复杂数据类型而言,复制了一份栈内存中的地址。

深拷贝

对于复杂数据类型而言,开辟了一块新的堆内存并且在栈内存里得到一份新的指向地址。新旧数据改变互不影响。对于简单数据类型而言,复制了一份栈内存中的value。

相关基础

对于拷贝我们需要知道的基础知识就是数据类型。如果是简单数据类型那么我们直接返回就好了,如果是复杂数据类型我们要做的就是开辟一个新的内存去存储该数据再返回指针地址

数据类型

JavaScript 语言中类型集合由原始值对象组成。

原始值

  • 原始值(也称简单数据类型基础数据类型
  1. 布尔类型(Boolen)
  2. Null 类型
  3. Undefined 类型
  4. 数字类型(Number)
  5. BigInt 类型(Number)
  6. 字符串类型(String)
  7. 符号类型(Symbol)

对象

  • 对象(也称复杂数据类型引用类型
  1. 标准普通对象:object
  2. 标准特殊对象:ArrayRegExpDateMathError……
  3. 非标准特殊对象:NumberStringBoolean……
  4. 可调用/执行对象「函数」:function

不同数据类型的存储

  • 原始值(简单数据类型):由于大小固定且体积较小存在栈内存(stack)中,其变量声明既是栈中的key,value既是我们所熟知的值(值既是地址)
  • 对象(复杂数据类型):由于大小不固定且体积一般较大所以存储在堆(heap)中,其变量声明既是栈中的key,value既是我们所熟知的地址(指向堆中那块数据的哈希)

1.png 注:本图片引用自(zhuanlan.zhihu.com/p/50206683)

dataType.png 注:上图总结全面,知识点丰富!(segmentfault.com/a/119000001…

有了以上基本知识点介绍我们可以开始实现深拷贝函数

1.相关辅助函数

/**
 * @des 输入任意数据类型,返回数据类型
 * @param data: 任意数据
 * return 数据类型 eg:Object\String\Number...
 */
const getType = data => {
  return Object.prototype.toString.call(data).slice(8, -1)
}

/**
 * @des 输入任意数据类型,判断是否是引用类型(除null)
 * @param data: 任意数据
 * return Boolen
 */
const isObject = data => {
  return (typeof data === 'object' && data !== null) || getType(data) === 'Function'
}

2.主体函数

/**
 * @des 深拷贝(cloneDeep函数需要使用到上方两个协助函数)
 * @param data: 任意数据
 * @param hash: 哈希
 * return 新数据
 */
const cloneDeep = (data, hash = new WeakMap()) => {
  // 第一步:判断是简单数据类型还是复杂数据类型,若是简单数据类型直接返回
  if (!isObject(data)) return data
  
  // 第二步:获取准确的类型
  const targetType = getType(data)
  
  // 第三步:查询是否已经存在此对象,存在找出(此处处理循环引用问题)
  if (hash.has(data)) return hash.get(data)
  
  // 第四步:函数处理
  if (targetType === 'Function') {
    return function() {
      // 带入原作用域链以及参数
      return data.call(this, ...arguments)
    }
  }
  
  // 第五步:正则处理
  if (targetType === 'RegExp') {
    const reg = new RegExp(data.source, data.flags)
    // 连续执行test()函数,关注lastIndex属性
    if (data.flags) {
      reg.lastIndex = 0
    }
    return reg
  }
  
  // 第六步:关于日期对象处理
  if (targetType === 'Date') return new Date(data)
  
  // 第七步:关于Set数据处理
  if (targetType === 'Set') {
    const newSetTarget = new Set()
    data.forEach(v => {
      newSetTarget.add(v)
    })
    return newSetTarget
  }
  
  // 第八步:关于Map数据处理
  if (targetType === 'Map') {
    const newMapTarget = new Map()
    data.forEach((v, k) => {
      if (isObject(v)) {
        newMapTarget.set(k, cloneDeep(v))
      } else {
        newMapTarget.set(k, v)
      }
    })
    return newMapTarget
  }
  
  // 第九步:关于Symbol
  if (targetType === 'Symbol') return Object(Symbol.prototype.valueOf.call(data))
  
  // 第十步:数组/对象处理
  let copiedData = targetType === 'Array' ? [] : {}
  
  // 第十一步:存哈希对象(处理自循环引用,用WeakMap主要鉴于其唯一性及v8的垃圾回收机制考虑)
  hash.set(data, copiedData)
  
  // 第十二步:循环处理
  for (let key in data) {
    // 目前只取显现属性(可扩展:如果有特殊需求可以读取其他属性)
    if (data.hasOwnProperty(key)) {
        // 判断是简单数据类型还是复杂数据类型
      if (isObject(data[key])) {
        // 复杂对象递归处理
        copiedData[key] = cloneDeep(data[key], hash)
      } else {
        // 简单数据类型
        copiedData[key] = data[key]
      }
    }
  }
  
  // 第十三步:返回
  return copiedData
}

通过以上13个步骤就基本完成了对深拷贝函数cloneDeep的实现。东西不多注释也写清楚了,如果还有不明白的可以留言或者可以通过断点运行来协助理解。自己理解了才是收获哈!

相关涉及知识

  • 数据类型相关
  • 堆栈
  • es6默认参数
  • call/apply
  • es6-weakmap\set\map\symbol

相关文章推荐

最后

原创不易,请多包涵,加油~ o( ̄▽ ̄)o。