地址与地基:在 JavaScript 的堆栈迷宫里,重新理解“复制”的哲学

前言

js 复制代码
let a = 1
let b = a
let obj = {
  age: 18
}
let obj2 = obj

把 a 的值赋值给 b ,这个是一种拷贝,但是把 obj 的值赋给另一个对象 obj2 ,它就不是拷贝,这是为啥呢,今天我们来深入探讨 js 中的 拷贝 --- 分为浅拷贝和深拷贝,由于原始类型在任何语言层面操作都是值复制,没有共享问题,也就没有"深浅"维度;所以在这里,拷贝只针对引用类型,基于原对象,拷贝得到一个新对象,这也解释了为什么上面对象赋值不算拷贝。


浅拷贝

1、[].slice(0) ---截取数组

[].slice(0) 就是一种浅拷贝,我们来看看一下浅拷贝都有些什么特征

js 复制代码
const arr = ['a', 'b', 'c', 'd', {age: 18}]    
const arr2 = arr.slice(0)
arr2.splice(2, 1)
console.log(arr);   

看输出结果,它符合基于原对象,拷贝得到一个新对象,新对象改动不影响旧对象,但是这样也解释不了为啥叫浅拷贝呀,别急,我们再来看一段代码:

js 复制代码
const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0)
arr[4].age = 20
console.log(arr2);

我们可以看到,旧对象里面的子对象在拷贝过后改动会影响新对象里面的子对象,只是因为所有的引用类型都会在堆里放一个引用地址,我们拷贝时新对象里面的子对象是 copy 了一个引用地址,后续子对象里面的值更改过后,新对象也会跟着变,如下图所示:

所以我们可以知道 浅拷贝 --- 新对象受原对象的影响(只拷贝了原对象的第一层,里面的子对象拷贝的还是引用地址)。实现浅拷贝的方法有五种,分别是 [].slice(0)[...arr]arr1.concat(arr2)arr.toReversed().reverse()Object.assign({}, obj)


2、 [...arr] ---解构数组

js 复制代码
const a = [1, 2, {age: 18}]
let c = [...a]
c.splice(1, 1)
a[2].age = 19
console.log(a);
console.log(c);

3、 arr1.concat(arr2) --- 合并数组

js 复制代码
const a = [1, 2, 3, {age: 18}]
const b = [4, 5]
const c = a.concat(b)
console.log(c, a);
const d = [].concat(a)
a[3].age = 20
console.log(d);

4、 arr.toReversed().reverse() --- 反转数组

.toReversed() 得到一个反转的新数组,.reverse()方法会反转原数组,配合起来就能实现浅拷贝:

js 复制代码
const arr = [1, 2, 3, {age: 18}]
const arr2 = arr.toReversed().reverse()
arr2.splice(1, 1)
arr[3].age = 20
console.log(arr2);
console.log(arr);

5、 Object.assign({}, obj) --- 合并对象

它和合并数组不一样,它把后面对象里的值放入前面的对象里面,覆盖前面的对象里的值,返回一个新对象,后面对象不变,所以在这里实现拷贝效果都是在前面放一个空对象

js 复制代码
const obj = {
  name: '俊杰',
  like: ['泡脚']
}
const newObj = Object.assign({}, obj)
obj.like[0] = '台球'
console.log(newObj);

手搓浅拷贝

js 复制代码
const obj = {
  name: '俊杰',
  age: 18,
  like: {
    n: '洗脚',
    m: '台球'
  }
}
function shallowCopy(obj) {
  let o = {}
  // 遍历原对象,将原对象中的 key,value 都存到新对象中一份
  for (let key in obj) {   // key 为形参
    if (obj.hasOwnProperty(key)) {  // 判断当前的 key 是否是 obj 显示属性
      o[key] = obj[key]    // o.name = obj[name] = '俊杰'
    }
  }
  return o
}
const newObj = shallowCopy(obj)
obj.like.m = '篮球'
console.log(newObj);

深拷贝

顾名思义:层层拷贝,新对象不受原对象修改的影响,深拷贝有两种方法 structuredClone()JSON.parse(JSON.stringify(obj))

1、structuredClone()

js 复制代码
const obj = {
  name: '俊杰',
  age: 18,
  like: {
    n: '洗脚',
    m: '台球'
  },
  a: 123n,
}
const newObj = structuredClone(obj)
obj.like.m = '蓝球'
console.log(newObj);

它可以把子对象里面的东西全给 copy 出来,但是他有一个缺陷,就是无法拷贝函数和 Symbol。


2、 JSON.parse(JSON.stringify(obj))

js 复制代码
const obj = {
  name: '俊杰',
  age: 18,
  like: {
    n: '洗脚',
    m: '台球'
  }
 }
const oo = JSON.parse(JSON.stringify(obj))
obj.like.m = '篮球'
console.log(oo);


JSON.stringify(obj) 这是将里面的对象变为字符串,JSON.parse(str) 则是将里面的字符串变为对象,所以合起来实现了深拷贝,同样的他也有缺陷,无法处理 bigint, undefined, NaN, Infinity, function,Symbol。

手搓深拷贝

js 复制代码
const obj = {
  name: '俊杰',
  age: 18,
  like: {
    n: '洗脚',
    m: '台球'
  }
}
function deepClone(obj) {
  let o = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    // 判断里面是否有引用类型例如对象,数组(函数除外),null也要除外
      if (typeof(obj[key]) == 'object' && obj[key] !== null) {
      // 如果是引用类型就再执行一次拷贝
        const childObj = deepClone(obj[key])
        o[key] = childObj                      
      } else {
        o[key] = obj[key]
      }
    }
  }
  return o
}
const newObj = deepClone(obj)
obj.like.m = '篮球'
console.log(newObj);

运用递归思想,进行层层拷贝。


结语

拷贝,看似只是"复制"二字,实则藏着 JS 内存模型的全部秘密。

浅拷贝,把引用地址递出去,像递名片------改的是同一份底稿;

深拷贝,把整座房子连地基都搬过来,从此山水不相逢。

下次再写 let obj2 = obj 时,不妨先问一句:

"我只是想要一把钥匙,还是想要一栋真正属于自己的房子?"

弄懂深浅,才能让你的数据不再"藕断丝连",也让你的代码少一句 console.log 的叹息。

相关推荐
杨啸_新房客1 小时前
如何优雅的设置公司的NPM源
前端·npm
ohyeah1 小时前
深入理解 JavaScript 中的继承与 instanceof 原理
前端·javascript
linhuai1 小时前
flutter如何实现有登陆权限管理
前端
crary,记忆1 小时前
React 之 useEffect
前端·javascript·学习·react.js
BD_Marathon1 小时前
【JavaWeb】HTML常见标签——标题段落和换行
前端·html
小飞侠在吗1 小时前
vue OptionsAPI与CompositionAPI
前端·javascript·vue.js
春卷同学1 小时前
基于Electron开发的跨平台鸿蒙PC找不同游戏应用
javascript·游戏·electron
天涯路s1 小时前
qt怎么将模块注册成插件
java·服务器·前端·qt
只与明月听1 小时前
FastAPI入门实战
前端·后端·python