新手学前端JS浅拷贝和深拷贝:对象复制竟然是个“替身文学”?

前言

写 JS 谁没栽在拷贝上?兴冲冲赋值以为复制好了对象,改新变量原数据跟着原地叛变;只改内层属性老对象悄悄躺枪,排查 BUG 半天才发现踩了引用的大坑。浅拷贝只能扒外层皮,嵌套对象依旧共用一套内存,深拷贝稍有不慎又遇上正则、日期、函数翻车。 本文用接地气的大白话,掰开深浅拷贝底层原理,从扩展运算符、Object.assign 到递归手写深拷贝、JSON 暴力拷贝逐个拆坑,理清适用场景与边界缺陷,看完再也不会被引用数据偷偷背刺。

递归

在说关于js拷贝之前,我们先来了解一下递归这个概念,因为深拷贝就是利用了递归思想。创造递归有两个条件:

  1. 找公式:即找递归的公式
  2. 找出口:即找递归的出口,下面举个斐波那契数列的例子加以深刻了解
js 复制代码
function feibo(n){
    if(n ==1 || n ==2) return 1
    return feibo(n - 1) + feibo(n - 2)
}
console.log(feibo(10))

可以看到feibo(n - 1) + feibo(n - 2)就是该递归函数的公式,if(n ==1 || n ==2) return 1就是递归出口,没有这个出口,递归将会一直执行,直到调用栈报错,还有一点值得注意,并不是所有的递归都需要出口。

拷贝

拷贝顾名思义就是克隆一份原对象,得到一份新对象,用一段代码举例:

js 复制代码
let a = 1
let b = a
a = 2
console.log(b);

这时我们知道js经过预编译之后,已经将a的值赋给了b,也就可以理解为b将a的数据拷贝了一份传给自己保存,从而使得a的值改变之后b的值不变。这就是拷贝,那对象等引用类型的拷贝也一样吗,显然不是的,那就要引出我们的浅拷贝和深拷贝的概念了。

1. 浅拷贝

浅拷贝通俗来讲,就是如果原obj对象里面是基本类型,直接给新对象赋值原始类型,如果是引用类型,则给新对象赋值的是原对象的引用地址,只复制第一层,里面嵌套对象不复制,这就好像复刻一个房子,但是家具还共用,下面举个例子加以说明:

js 复制代码
let obj ={
    age:18
}
let oo =obj
obj.age = 19
console.log(oo.age);

就比如这里,新手很容易以为这和原始类型一样,将对象里的age重新复制后,拷贝的新对象oo里的age也不会变,但其实就是在预编译时两份对象存储的都是同一份引用地址,指向的都是同一个堆空间,所以obj.age = 19执行之后,等于修改同一个对象,age都会变成18。就像下图所示:

那么js有什么方法实现浅拷贝呢?

-Object.assign 实现浅拷贝

js提供了一个专门用来浅拷贝的函数Object.assign(),传入参数为Object.assign({},obj),我们可以试着手搓一个丐版的浅拷贝:

js 复制代码
let obj ={
    age:18,
    name: '南哥',
    hobby:['瓦','州']
}
// let obj = ['a','b','c']
function shallowCopy(obj){
   let newObj = Array.isArray(obj)? [] : {}
   for(let key in obj){
    if(obj.hasOwnProperty(key)){
           newObj[key] = obj[key]
    }
   }
   return newObj
}
let oo = shallowCopy(obj)
 obj.hobby[0] = '原始'
console.log(oo);

执行结果为:

当我加入 obj.hobby[0] = '原始'的执行后,结果为:

可以看到拷贝过来的的对象hobby数组变了,因为拷贝的是引用地址,而地址对应的堆中hobby数组变了。所以

Object.assign({},obj),\[\].slice() 原obj对象里面如果是基本类型,直接给新对象赋值,如果是引用类型,则给新对象赋值的是原对象的引用地址 \[\].slice()

深拷贝

好了,你已经知道浅拷贝原理,那么类似的深拷贝就是从第一层复制到最后一层,但是所有引用地址全部重新创建,还是举个例子结合递归带你了解丐版底层深拷贝原理:

js 复制代码
let obj ={
    age:18,
    name: '南哥',
    hobby:{
        n:'1',
        m:'2',
        o:{
            a:'洗脚'
        }
    }
}

function deepCopy(obj){
    let newObj = {}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
           if(typeof obj[key] == 'object' && obj[key] !== null){
            newObj[key] = deepCopy(obj[key])
           }else{//若为原始值
            newObj[key] = obj[key]
           }
    }
    }
    return newObj
}
const oo = deepCopy(obj)
obj.hobby.o.a = '按摩'
console.log(oo);

结果为:

可以看到,修改obj.hobby.o.a 之后,新对象里的不会变,这时因为新对象也创建了一个新的引用地址,不受影响。为什么递归能实现深拷贝,递归会不断进入: 直到:不是对象,停止递归。然后逐层返回。 因此:

复制代码
所有子对象
都获得新的引用地址

js还有两种自带的深拷贝

JSON.parse(JSON.stringify(obj)) //不能拷贝bigint类型,undefined类型,NaN类型,Infinity类型,函数类型,symbol类型
structuredClone(obj) //不能拷贝函数类型

相关推荐
葫芦和十三5 小时前
图解 MongoDB 18|复制集拓扑:Primary、Secondary 和 Arbiter 的分工
后端·mongodb·面试
To_OC7 小时前
LC 994 腐烂的橘子:人人都说是 BFS 入门题,我却写了三遍才过
javascript·算法·leetcode
葫芦和十三11 小时前
图解 MongoDB 15|journal 与持久化:写入怎么不丢,崩溃怎么恢复
后端·mongodb·面试
葫芦和十三11 小时前
图解 MongoDB 16|压缩:snappy、zstd 和 zlib 的取舍
后端·mongodb·面试
To_OC13 小时前
LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了
javascript·算法·leetcode
labixiong15 小时前
实现一个能跑的迷你版Promise(一)
前端·javascript·面试
weedsfly19 小时前
还在用 Axios?你可能需要重新理解 XHR 与 Fetch
前端·javascript·面试
CoderWeen19 小时前
从零实现一个 Vue3 流程图编辑器:节点拖拽、贝塞尔连线与框选
前端·javascript
Hyyy21 小时前
什么是bun?和pnpm有什么区别
前端·面试·bun
To_OC1 天前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode