【JS】详解深浅拷贝

2 阅读4分钟

前言

前端开发中处理对象和数组时,我们经常会碰到拷贝的概念。拷贝又常常分为深拷贝和浅拷贝,弄清楚两者的概念及区别尤为重要。

拷贝就是创建一个数据的副本,使得两者具有相同的值,但在内存中是独立存在的。简单来说,浅拷贝创建的副本会受原数据的影响,可能原数据身上的某个值变了,副本身上对应的值也要跟着变。而深拷贝就不会受到原数据的影响,独善其身。那么详细的请看下文。

浅拷贝

先看代码:

let obj = {
    age: 18
}
let obj2 = obj 
obj.age = 20 
console.log(obj2.age);

这里是不是就是一个浅拷贝,创建一个新对象(obj2),当原对象(obj)的age属性的值改变了,obj2age属性也跟着改变。

定义: 浅拷贝是指创建一个新的数据结构,该数据结构与原始数据结构具有相同的基本元素,但对于对象或数组,仅复制它们的引用地址而不是实际的数据。 所以新老对象共享里面的属性,新对象一改变,老对象也会被影响。

换句话说,let obj2 = obj 仅仅把obj的引用地址复制给了obj2,所以也能解释为什么obj可以影响obj2。

浅拷贝方法

常见的方法比较多:Object.create(x)、Object.assign({}, x)、concat、slice、数组解构、 arr.toReversed().reverse()

1. Object.create(x)

let a = { name: '小明' }
let b = Object.create(a) 

Object.create() 是对象身上自带的方法。可以创建一个空对象b,且b对象能隐式的继承到a对象的属性。

2. Object.assign({}, x)

let a = { name: '小明' }
let b = Object.assign({}, a) 

Object.assign() 是对象身上自带的方法。对象b将对象a的属性复制下来。

3. concat

let arr = [1,2,3,{a: 10}]
let newArr = [].concat(arr)

该方法是数组身上的一个方法,可以合并新老数组。

4. slice

let arr = [1,2,3,{a: 10}]
let newArr = arr.slice(0)

该方法是数组身上的一个方法,可以切割老数组达到浅拷贝的效果。

5. 数组解构

let arr = [1,2,3,{a: 10}]
let newArr = [...arr]

6. arr.toReversed().reverse()

let arr = [1,2,3,{a: 10}]
let newArr = arr.toReversed().reverse()

手写浅拷贝

代码:

function shalldowCopy(obj) {
    if (typeof obj !== 'object' || obj === null) return //只拷贝引用类型
    let objCopy = obj instanceof Array ? [] : {}
    for (let key in obj) {
        if(obj.hasOwnProperty(key)) {
            objCopy[key] = obj[key]
        }
    }
    return objCopy
}

浅拷贝的核心是只要复制了对象的一层,而不是递归地复制所有嵌套的子对象。所以只要把原对象(原数组)的引用地址赋给新对象。拷贝前判断原数据的类型,如果不是引用类型直接返回。再通过三元运算符初始化新对象的类型。最后通过for...in来遍历对象,但是该方法可能会遍历原对象原型链上的属性,所以拷贝前用JS自带的hasOwnProperty方法判断一下。

深拷贝

那么了解了什么是浅拷贝后,理解深拷贝就更加容易了。

深拷贝会将原始数据结构中的所有元素以及元素所包含的所有子元素都进行复制,而不仅仅是复制引用。换句话说,新数据与老数据完全独立,老数据也不会影响新数据的值。

深拷贝的方法

JSON.parse(JSON.stringify( ))

let obj = {
    name: '阿伟',
    age: 18,
    like: {
        type: 'coding'
    }
}

//深拷贝
let newObj = JSON.parse(JSON.stringify(obj))

obj.like.type = 'eating'
console.log(newObj.like.type) //'coding'

该方法是先通过JSON.stringify将原数据转为JSON字符串,再通过JSON.parse将JSON字符串转为新对象,且新对象会将原对象身上所有的键值对拷贝下来,且不会共享内部引用,也就是深拷贝。

但是该方法也有一些局限性

1. 无法拷贝某些数据类型

let obj = {
    name: '阿伟',
    age: 18,
    like: {
        type: 'coding'
    },
    a: undefined,
    b: null,
    c: function() {},
    d: {
        n: 100
    },
    e: Symbol('hello') 
}

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // 

该方法无法拷贝函数、正则表达式、日期对象、Map、Set等特殊对象,undefined、Symbol类型也不行。

2. 无法拷贝对象中的循环引用

let obj = {
    name: '阿伟',
    age: 18,
    like: {
        type: 'coding'
    }
}

//循环引用
obj.c = obj.d
obj.d.m = obj.c

let newObj = JSON.parse(JSON.stringify(obj)) //TypeError

对象中的循环引用(对象内部引用自身)无法通过JSON.stringify()进行序列化,会导致报错或结果不完整。

手写深拷贝

代码:(以数组和对象为例)

function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) return
    let objCopy = obj instanceof Array ? [] : {}
    for (let key in obj){
        if(obj.hasOwnProperty(key)){
            if (obj[key] instanceof Object) {
                objCopy[key] = deepCopy(obj[key])
            } else{
                objCopy[key] = obj[key]
            }
        }
    }
    return objCopy
}

深拷贝的核心是递归地复制所有了嵌套的子对象objCopy[key] = deepCopy(obj[key])递归调用每一层嵌套的对象,直到为原始数据类型则返回。

Lodash

Lodash提供的_.cloneDeep方法实现的深拷贝弥补了JSON方法的局限性,能够处理各种类型的值,并且它也能够处理循环引用的情况。

使用实例:

var objects = [{ 'a': 1 }, { 'b': 2 }];
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]); // false 

详细可以访问:lodash.cloneDeep

最后

在实际编程中,正确地选择和使用拷贝操作可以帮助开发人员避免出现数据共享导致的错误,确保程序的正确性和稳定性。希望本文能让大家对拷贝有更深的理解。

欢迎评论区留言!