一、数据类型

  • 基本数据类型 (String, Number, Boolean, Null, Undefined,Symbol)
  • 引用数据类型 (Object,包含 function,Array,Date等)

1、基本数据类型的特点:直接存储在栈内存中的数据
2、引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
在这里插入图片描述

二、浅析栈和堆

通过栈里面定义一个地址值,通过地址值去找堆里面定义的某一个值,
很重要一点是他的栈里是个地址值,地址值指向的是堆,他在堆里面定义的某一个值
相当于拿着号,去堆里面去找,两个号也就是地址值其实是一模一样的

堆跟栈最大的区别是:
1)堆在栈里存了一个地址值
2)栈存储的永远是一个基本数据类型的数据

三、对深浅拷贝理解

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精准拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。这个内存地址指向同一个堆内存。如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:创建一个新对象,如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,则从堆内存中开辟一个新的区域存放该引用类型指向的堆内存中的值,修改新对象的值不会影响原对象。(对原始对象的所有属性进行递归,对所有的引用类型的属性同样开辟新区域)

浅拷贝和深拷贝引用其他博主图片,示意图大致如下:

在这里插入图片描述

在这里插入图片描述
总之,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会修改到原对象。

四. 浅拷贝实现方法

1)ES6语法展开运算符…

    const obj1 = {
      name: "icy",
      age: 20,
      hobbies: ["eat", "sleep", "game"],
    };
    const obj2 = { ...obj1 }
    console.log(obj2, 'obj2')

打印结果
在这里插入图片描述
为了验证是浅拷贝,我们改变一下obj2中数组的第一项的值,然后再输出ojb1和obj2:

const obj1 = {
      name: 'Nancy',
      age: 18,
      hobbies: ['eat', 'sleep', 'game']
    }
    const obj2 = { ...obj1 }

    // 修改堆内存中的值
    obj2.age = 20
    obj2.hobbies[0] = 'play'

    console.log('修改后obj2', obj2)
    console.log('修改前obj1', obj1)

打印结果如下:

在这里插入图片描述

修改数组时候,obj1和obj2都受到了影响,验证了浅拷贝。

2)Object.assign() 方法

Object.assign() 方法用于对象的合并,将源对象(souce)的所有可枚举属性,复制到目标对象(target)。然后返回目标对象,返回值为合并后的新对象。Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。
但是Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

注意: 当object只有一层的时候,是深拷贝。

  let obj = { username: 'kobe' }
  let obj2 = Object.assign({}, obj)   //将拷贝对象与{}空对象合并
  obj2.username = 'wade'
  
  console.log(obj,'obj')
  console.log(obj2,'obj2')

在这里插入图片描述

拓展:
Object.assign() 拷贝的属性是有限制的。

  • 只拷贝源对象的自身属性(不拷贝继承属性
  • 只拷贝可枚举的属性(enumerable: true), 忽略enumerable为false的属性。

3)Array.prototype.concat() 方法

该方法用于数组合并,合并的数组.concat(被合并的数组…)
参数可有多个,用逗号分隔,返回合并后的数组。
原理:用原数组去合并一个空数组,返回合并后的数组。

let arr = [1, 3, {
   username: 'kobe'
}];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
arr2[0] = 555;

console.log(arr,'arr');
console.log(arr2,'arr2');

在这里插入图片描述

4)数组剪裁方法 slice()

该方法用途很多,可对数组进行增删,剪裁操作。

 const arr1 = [1, 3, { username: 'kobe' }]
 const arr2 = arr1.slice()  //返回剪裁后的数组,这里没有剪裁掉任何项,相当于返回原数组

 // 修改堆内存中的值
 arr2[0] = 5
 arr2[2].username = 'wade'

 console.log('arr1', arr1)
 console.log('arr2', arr2)

在这里插入图片描述

关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。

个人感觉,以上四种方法都只有一层的时候才是深拷贝。

5)手写浅拷贝

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

五. 深拷贝实现方法

1)JSON.parse(JSON.stringify())

原理:用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

  const obj1 = {
    user_info:{
      name: "Nancy",
      age: 18,
      gender: "女",
    },
    hobbies: ["eat", "sleep", "game"],
  }
    const obj2 = JSON.parse(JSON.stringify(obj1)); 
    obj2.user_info.name = 'Juliet'
    
    console.log(obj1,'obj1');
    console.log(obj2,'obj2')

在这里插入图片描述

注意点:
估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

1) 无法解决循环引用的问题。

举个例子:

const a = {val:2};
a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

2)无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等。(正则变为空对象)
3)无法拷贝函数。(函数变为null)

const obj1 = {
      name: "Nancy",
      age: 18,
      gender: "女",
      hobbies: ["eat", "sleep", "game"],
      //函数
      watchComic: () => {
        console.log("Nancy 你好");
      },
      //正则
      regx: /^icy{3}$/g,
    };
    const obj2 = JSON.parse(JSON.stringify(obj1)); 
    console.log(obj2,'obj2');

在这里插入图片描述

可看到上图打印,函数没了,正则变成空对象{}。
这是因为 JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。

因此这个api先pass掉,我们继续重新写一个深拷贝。

2)手写递归方法

递归方法实现深度克隆原理:递归遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

手动递归实现深拷贝,我们只需要完成以下几点即可:

1) 对于基本数据类型,我们只需要简单地赋值即可(使用 “=” )
2) 对于引用类型,我们需要创建新的对象,并通过 遍历键 来赋值对应的值,这个过程中如果遇到 Object 类型还需要再次进行递归遍历。(注意Array也属于Object类型)
3)解决循环引用的问题
4)拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等。
5)拷贝函数

简易版如下:

  function deepClone(source: any): any {
    if (!source || typeof source !== 'object') {
      throw new Error('不是对象类型,不能深拷贝')
    }
    const targetObj: any = Array.isArray(source) ? [] : {}
    Object.keys(source).forEach((key: string) => {
      if (source[key] && typeof source[key] === 'object') {
        targetObj[key] = deepClone(source[key])
      } else {
        targetObj[key] = source[key]
      }
    })
    return targetObj
  }

现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。

1) 解决循环引用

现在问题如下:

let obj = {val : 100};
obj.target = obj;

deepClone(obj);//报错: RangeError: Maximum call stack size exceeded

这就是循环引用。我们怎么来解决这个问题呢?

首先创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。

  const isObject = source => (typeof source === 'object' || typeof source === 'function') && source !== null

  const deepClone = (source: any, map = new Map()) => {
    if (map.get(source)) {
      return source
    }
    if (isObject(source)) {
      map.set(source, true)
      const targetObj: any = Array.isArray(source) ? [] : {}
      Object.keys(source).forEach((key: string) => {
        if (source[key] && typeof source[key] === 'object') {
          targetObj[key] = deepClone(source[key], map)
        } else {
          targetObj[key] = source[key]
        }
      })
      return targetObj
    }
  }

现在来试一试:

 const a = { val: 100 }
 a.target = a
 let newA = deepClone(a)
 console.log(newA, 'clone循环引用') //{ val: 100, target: { val: 100, target: [Circular] } }

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是 map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:

  • 在计算机程序设计中,弱引用与强引用相对, 是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 --百度百科
  • 弱引用意味着当一个对象作为键被引用时,如果没有其他地方引用这个对象了,垃圾回收机制会自动将该对象从 WeakMap 中删除,释放内存。换句话说,WeakMap 不会阻止对象被垃圾回收,因此它适用于需要临时存储对象的场景。
  • 由于键是弱引用的,所以 WeakMap 没有提供像 Map 那样的迭代方法,也没有 size 属性。此外,WeakMap 的键必须是对象,基本类型的键是不支持的。

说的有一点绕,用大白话解释一下,被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。

稍微改造一下即可:

  const deepClone = (source: any, map = new WeakMap()) => {
   // ...
  }

2)拷贝特殊对象

对于特殊的对象,我们使用以下方式来鉴别:

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么结果:

["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]

不可遍历的对象:

const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的处理。

const handleRegExp = (target: any) => {
    const { source, flags } = target
    return new target.constructor(source, flags)
  }
  const handleNotTraverse = (source: any, tag: any) => {
    let Ctor = source.constructor
    switch (tag) {
      case boolTag:
        return new Object(Boolean.prototype.valueOf.call(source))
      case numberTag:
        return new Object(Number.prototype.valueOf.call(source))
      case stringTag:
        return new Object(String.prototype.valueOf.call(source))
      case symbolTag:
        return new Object(Symbol.prototype.valueOf.call(source))
      case errorTag:
      case dateTag:
        return new Ctor(source)
      case regexpTag:
        return handleRegExp(source)
      default:
        return new Ctor(source)
    }
  }

3)拷贝函数

函数直接赋值就行,因为一个已定义的函数是没法被改变的。

4)深拷贝完整代码

  const getType = obj => Object.prototype.toString.call(obj)
  const isObject = source => (typeof source === 'object' || typeof source === 'function') && source !== null

  const canTraverse = {
    '[object Map]': true,
    '[object Set]': true,
    '[object Array]': true,
    '[object Object]': true,
    '[object Arguments]': true
  }
  const mapTag = '[object Map]'
  const setTag = '[object Set]'
  const boolTag = '[object Boolean]'
  const numberTag = '[object Number]'
  const stringTag = '[object String]'
  const symbolTag = '[object Symbol]'
  const dateTag = '[object Date]'
  const errorTag = '[object Error]'
  const regexpTag = '[object RegExp]'

  const handleRegExp = (target: any) => {
    const { source, flags } = target
    return new target.constructor(source, flags)
  }
  const handleNotTraverse = (source: any, tag: any) => {
    let Ctor = source.constructor
    switch (tag) {
      case boolTag:
        return new Object(Boolean.prototype.valueOf.call(source))
      case numberTag:
        return new Object(Number.prototype.valueOf.call(source))
      case stringTag:
        return new Object(String.prototype.valueOf.call(source))
      case symbolTag:
        return new Object(Symbol.prototype.valueOf.call(source))
      case errorTag:
      case dateTag:
        return new Ctor(source)
      case regexpTag:
        return handleRegExp(source)
      default:
        return new Ctor(source)
    }
  }
  const deepCloneFull = (source: any, map = new WeakMap()) => {
    if (!isObject(source)) {
      return false
    }
    let type = getType(source)
    console.log(type, 'type')
    let targetObj: any
    console.log(canTraverse[type], 'canTraverse[type]')
    if (!canTraverse[type]) {
      // 处理不能遍历的对象
      return handleNotTraverse(source, type)
    } else {
      // 这波操作相当关键,可保证对象的原型不丢失
      let ctor = source.constructor
      targetObj = new ctor()
    }

    if (map.get(source)) {
      return source
    }
    map.set(source, true)

    if (type === setTag) {
      // 处理Set
      source.forEach((item: any) => {
        targetObj.add(deepCloneFull(item, map))
      })
    }
    if (type === mapTag) {
      // 处理Map
      source.forEach((item: any, key: any) => {
        targetObj.set(deepCloneFull(key, map), deepCloneFull(item, map))
      })
    }

    // 处理数组和对象
    Object.keys(source).forEach((key: string) => {
      if (source[key] && typeof source[key] === 'object') {
        targetObj[key] = deepCloneFull(source[key], map)
      } else {
        targetObj[key] = source[key]
      }
    })
    return targetObj
  }

对深拷贝方法做个测试:

  //   使用深拷贝 - 对象
  const oldObj = {
    user_info: {
      name: 'Nancy',
      age: 18,
      gender: '女'
    },
    hobbies: ['eat', 'sleep', 'game'],
    watchComic: () => {
      console.log('Nancy 你好')
    },
    regx: /^icy{3}$/g,
    set: new Set(),
    map: new Map(),
    date: new Date()
  }
  const newObj = deepCloneFull(oldObj)
  newObj.user_info.name = '铁锤妹妹'
  newObj.hobbies[0] = 'photo'
  console.log(newObj, 'newObj')
  console.log(oldObj, 'oldObj')

在这里插入图片描述

可以看到不管是对象、数组、Date、正则还是函数,都已经成功拷贝了。

参考:
能不能写一个完整的深拷贝?
彻底讲明白浅拷贝与深拷贝
前端深拷贝与浅拷贝(附实现方法)
谁动了我的数据 | 程序员必备小知识

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐