javascript 对象的深拷贝与浅拷贝

今天我们来谈一谈对象的深拷贝和浅拷贝吧

我们都知道,js数据类型主要分为两大类 基础数据类型和引用(复杂)数据类型。

  • 基础数据类型存在于栈内存中,当被拷贝时,会创建一个完全相等的变量
  • 而引用数据类型存在于堆中,存储的是一个内存空间,而赋值给变量的,仅仅是这个内存空间的一个引用而已。而就会出现一个问题,当我们将一个对象赋值给另一个变量时,赋值的是对象的引用,必然导致两个变量都指向同一个内存空间,其中一个改变时,必然会影响到另一个。

浅拷贝和深拷贝的理解

什么是浅拷贝

简单理解就是
自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。

可以实现浅拷贝的方法
1、Object.assign()

  • object.assign 是es6新增的一个方法
  • 用于合并多个对象到目标对象中
  • 第一个参数是目标对象,后面的参数是源对象
var obj = {
     }
var source1 = {
     
	name: '张三',
	age: 18,
	like: {
     
		music: '老鼠爱大米'
	}
}
Object.assign(obj, source1)
console.log(obj)
source1.like.music= "沙漠骆驼"
console.log(obj)
// 输出
{
      name: '张三', age: 18, like: {
      music: '老鼠爱大米' } }
{
      name: '张三', age: 18, like: {
      music: '沙漠骆驼' } }

从上面可以看出,修改source1的like属性下的music属性值时,会对obj产生影响

2、扩展运算符

  • 扩展运算符可以展开对象或者数组中的选项 obj = { …obj2 }
var obj = {
     }
var source1 = {
     
	name: '张三',
	age: 18,
	like: {
     
		music: '老鼠爱大米'
	}
}
obj = {
      ...source1 }
console.log(obj)
source1.like.music= "沙漠骆驼"
console.log(obj)
// 输出
{
      name: '张三', age: 18, like: {
      music: '老鼠爱大米' } }
{
      name: '张三', age: 18, like: {
      music: '沙漠骆驼' } }

可以发现,和Object.assign() 一样的结果

同时,使用这两种方式还需要注意

  • 它不会拷贝对象的继承属性
  • 它只能拷贝可枚举的属性
  • 它可以拷贝 Symbol 类型的属性
var obj = {
     
	name: '张三',
	age: 18,
	sym: Symbol(123)
}
Object.defineProperty(obj, 'aaa', {
     
    value: '这是一个不可枚举的属性',
    enumerable: false // 设置为false代表此属性不可枚举
})
var obj2 = {
      ...obj }
console.log(obj2)
// 输出
{
      name: '张三', age: 18, sym: Symbol(123) }

从上面可以看出,symbol属性被拷贝了,但是属性aaa并没有被拷贝

3、数组拷贝方法
concat ()
使用方法我就不多说了,不懂得看我上一篇文章吧

var arr = [{
     name: '张三'}, 2, {
     age: 18}]
var brr = arr.concat()
console.log(brr)
arr[0].name = '李四'
console.log(brr)
// 输出
[ {
      name: '张三' }, 2, {
      age: 18 } ]
[ {
      name: '李四' }, 2, {
      age: 18 } ]

可以看出,concat()也是浅拷贝

slice()
使用方法见上一篇文章

var arr = [{
     name: '张三'}, 2, {
     age: 18}]
var brr = arr.slice()
console.log(brr)
arr[0].name = '李四'
console.log(brr)
// 输出
[ {
      name: '张三' }, 2, {
      age: 18 } ]
[ {
      name: '李四' }, 2, {
      age: 18 } ]

和concat一样,属于浅拷贝

扩展运算符
数组扩展运算符和对象得一模一样,我就不另外说了

深拷贝

我们可能大多时候最常用到的深拷贝就是JSON.stringify() 了。相信大家都用过
原理就是先将对象转为json字符串,再转换为对象。

var obj = {
     
    name: '张三',
    like: {
     
        music: '老鼠爱大米',
        color: 'red',
        animate: {
     
            name: '狗'
        }
    }
}
var obj2 = JSON.parse(JSON.stringify(obj))
obj.like.music = '沙漠骆驼'
obj.like.animate.name = '熊猫'
console.log(obj)
console.log(obj2)
// 输出
{
     
  name: '张三',
  like: {
      music: '沙漠骆驼', color: 'red', animate: {
      name: '熊猫' } }
}
{
     
  name: '张三',
  like: {
      music: '老鼠爱大米', color: 'red', animate: {
      name: '狗' } }
}

可以看出,更改obj 不会影响到拷贝出来的obj2

但是这种方法也存在很多问题

  1. 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失,导致无法拷贝到
  2. 拷贝 Date 引用类型会变成字符串
  3. 无法拷贝不可枚举的属性
  4. 拷贝 RegExp 引用类型会变成空对象
  5. 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null
var obj = {
     
    base: {
     
        name: '张三'
    }
}
obj.date = new Date(0)
obj.un = undefined
obj.nu = null
obj.infinity = Infinity
obj.nan = NaN
obj.sym = Symbol(123)
obj.reg = /^123/
Object.defineProperty(obj, 'aaa', {
     
    value: '这是一个不可枚举的属性',
    enumerable: false // 设为false代表不可枚举
})
var obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj)
console.log(obj2)

// 输出
{
     
  base: {
      name: '张三' },
  date: 1970-01-01T00:00:00.000Z,
  un: undefined,
  nu: null,
  infinity: Infinity,
  nan: NaN,
  sym: Symbol(123),
  reg: /^123/
}
{
     
  base: {
      name: '张三' },
  date: '1970-01-01T00:00:00.000Z',
  nu: null,
  infinity: null,
  nan: null,
  reg: {
     }
}

从上面可以看出

  • Date对象变成字符串了
  • infinity 和 NaN 变成null了
  • un属性的值是undefined,并没有被拷贝到,因为这个属性在序列号时消失了,sym属性也是一样消失了
  • RegExp引用类型变成空对象了
  • aaa这个不可枚举的属性也没有被拷贝到

那么,如何实现一个完整的深拷贝方法呢,那就要看我们该如何一一来解决以上问题了

  • 拷贝引用类型属性时,我们可以通过递归来循环拷贝
  • 如果属性是简单数据类型,包括undefined,infinity 和 NaN, 则直接递归赋值
  • 拷贝Date对象时,我们可以直接重新生成一个新的Date实例返回
  • 拷贝RegExp类型时,也是一样,直接生成一个新的RegExp实例返回
  • 拷贝Symbol属性和不可枚举的属性时,使用Reflect.ownKeys (什么是Reflect.ownKeys)方法
function isObject (obj) {
     
    if ((typeof obj === 'object' || typeof obj === 'function') && (obj !== null)) {
     
        return true
    }
}
function deepClone (obj) {
     
    if (obj.constructor === Date) {
     
        return new Date(obj) // 返回一个新实例
    }
    
    if (obj.constructor === RegExp) {
     
        return new RegExp(obj) // 返回一个新实例
    }

    let allDesc = Object.getOwnPropertyDescriptors(obj)
    // 遍历传入参数所有键的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

    for (let key of Reflect.ownKeys(obj)) {
      
        cloneObj[key] = (isObject(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key]) : obj[key]
    }

    return cloneObj
}
var obj = {
     
    base: {
     
        name: '张三'
    }
}
obj.date = new Date(0)
obj.un = undefined
obj.nu = null
obj.infinity = Infinity
obj.nan = NaN
obj.sym = Symbol(123)
obj.reg = /^123/
Object.defineProperty(obj, 'aaa', {
     
    value: '这是一个不可枚举的属性',
    enumerable: false // 设为false代表不可枚举
})

var obj2 = deepClone(obj)
console.log(obj)
console.log(obj2)

// 输出
{
     
  base: {
      name: '张三' },
  date: 1970-01-01T00:00:00.000Z,
  un: undefined,
  nu: null,
  infinity: Infinity,
  nan: NaN,
  sym: Symbol(123),
  reg: /^123/
}
{
     
  base: {
      name: '张三' },
  date: 1970-01-01T00:00:00.000Z,
  un: undefined,
  nu: null,
  infinity: Infinity,
  nan: NaN,
  sym: Symbol(123),
  reg: /^123/
}

从结果可以看出,我们实现的深拷贝函数确实实现了对象的一个深拷贝功能。

深拷贝与浅拷贝就写到这了。有问题欢迎指出

你可能感兴趣的:(javascript,javascript,js)