新手学前端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) //不能拷贝函数类型

相关推荐
YHL1 小时前
📚 JS执行机制(执行上下文 + 调用栈 + 编译流程)
前端·javascript
不简说1 小时前
这次真香!sv-print 可视化打印设计器更新:插件脚手架、Excel 导出、弹窗 API 三连发
前端·javascript·前端框架
无聊的老谢1 小时前
Web GIS 最佳实践:Vue 集成 Leaflet/OpenLayers 实现基站海量点位渲染
前端·javascript·vue.js
东风破_1 小时前
V8 如何执行你的代码——编译、上下文与调用栈
javascript
Aphasia3112 小时前
从内存模型看深浅拷贝
前端·javascript·面试
云水一下2 小时前
TypeScript 从零基础到精通(二):基础类型与类型系统
javascript·typescript
嵌入式ZYXC2 小时前
第1篇:《面试题:画一个STM32最小系统电路,每个元件的作用》
stm32·单片机·嵌入式硬件·面试·职场和发展
你怎么知道我是队长3 小时前
CRC校验C语言实现-CRC8、CRC16、CRC16的直接计算法、查表法
c语言·前端·javascript
meilindehuzi_a3 小时前
深入理解 JavaScript 执行机制:从编译阶段到调用栈底层实现
开发语言·javascript·ecmascript