JavaScript 6种浅拷贝2种深拷贝

0 阅读7分钟

前言 什么是拷贝,为什么我们需要拷贝 拷贝分为哪几种 分别适用于什么情况

在 JavaScript 的世界里,拷贝数据就像复制一把钥匙——看起来一模一样,但用起来可能打开同一扇门(引用传递),也可能打开不同的门(值传递)。你是不是也遇到过这样的困扰:明明拷贝了一个对象,修改原对象时新对象也跟着变了?这就是浅拷贝和深拷贝在"暗中较劲"。今天,我们就来彻底搞懂它们!从 V8 引擎的存储秘密,到 6 种浅拷贝妙招,再到深拷贝的终极方案,一篇带你通关数据拷贝的核心知识!

一、V8 数据存储机制:内存里的"双城记"

在 JavaScript 的内存世界里,存在着两座截然不同的城市,它们遵循着完全不同的生存法则,这就是 V8 引擎精心设计的栈内存(Stack)堆内存(Heap)

1. 什么是栈内存"

举个生动的例子
当你声明一个变量 let a = 42; 时,V8 引擎就像在流水线上快速组装好一辆数字汽车,并给它贴上标签"a"。这个数字 42 就被直接存放在栈内存中。

2.什么是堆内存"

举个生动的例子
当你创建一个对象 const obj = { name: 'Alice' }; 时,V8 引擎会在堆内存中开辟一块空间,像建造一座秘密基地一样存储这个对象,然后在栈内存中存放这个基地的"地址门票"(内存地址)。

二、数据类型的"内存身份证":基本类型 vs 引用类型

JavaScript 的数据类型就像拥有不同"内存身份证"的公民,决定着它们在内存世界中的生存方式。

1. 基本类型(Primitive Types):栈内存的"独行侠"

成员清单

  • Number
  • String
  • Boolean
  • null
  • undefined
  • Symbol
  • BigInt
2. 引用类型(Reference Types):堆内存的"社交达人"

成员清单

  • Object
  • Array
  • Function
  • Date
  • RegExp
  • Map/Set

拷贝

  • 复刻一个对象,和原对象长的一样

  • 浅拷贝:只拷贝对象的最外层,原对象的属性值修改会影响新对象

浅拷贝的6种方法

  1. Object.create()

  2. [].concat(arr)

  3. 数组解构[...arr]

  4. let arr2=arr.slice(0,arr.length)

  5. Object.assign({},obj)

  6. arr.toReversed().reverse()

第一种方法 Object.create()

原理
Object.create() 会创建一个新对象,并将新对象的原型(__proto__)指向传入的原型对象。这就像给新对象找了个"干爹",继承原型对象的属性。

开始之前,有个问题:所有的对象都有原型对象吗?

不是的哦,js中有其实一个方法是可以创建对象但没有原型对象

这个方法就是

Objecr.create(null) 能够得到一个没有原型的对象

let obj={
  a:1
}

let obj2=Object.create(obj)
console.log(obj2.a);

Object.create()可以让创建的新对象的对象原型(隐式原型)指向我们传进来的对象,于是我们打印obj.a的时候在obj的上找不到a,就会去obj的对象原型(隐式原型)上找a,obj的隐式原型是指向obj对象

第二种方法 [].concat(arr)

该方法可以创建一个新数组,其内容是两个数组拼接的,且对原数组没有影响

那么 [].concat(arr)就可以得到一个与原来数组一样的数组

第三种方法 数组解构[...arr]

let arr = [1, 2, 3];
const [x, y, z] = arr;
//解构对象
console.log(x, y, z);
console.log(...arr);

let arr2=[...arr]
console.log(arr2);

第四种方法 let arr2=arr.slice(0,arr.length)

数组的两种方法(splice)(slice),傻傻分不清

splice第一种用法

splice(0,1)第一个参数表示删除从第几个下标开始,第二个参数表示删除几个数组元素

splice第二种用法

作为增加数组元素的方法,前两个同理,第二个参上为0时,新加第三个参数,表示插入的元素

通过slice(0,arr.length)来拷贝数组,参数时左闭右开的

通过数组的解构来实现拷贝,先解构数组,再将解构的数组装到新数组中

第五种方法 Object.assign({},obj)

对象的拼接

可以拼接对象,但是会对原来的对象产生影响。

6. arr.toReversed().reverse()

使用arr.reverse()可以将数组反转,但是原数组会受到影响,数组新加了一个方法toReversed()可以返回一个反转一个数组且原数组不受影响

以上就是浅拷贝的6种方法

浅拷贝的与深拷贝的区别:

原对象内容发生变化,不是深拷贝

原因是因为引用数据是在堆内存中存储的,obj中存储的并不是like对象,而是like对象的地址值, Object.assign是将对象内容复制到新对象,复制对象里的对象时,复制的是地址。所以对对象的引用数据更改时,新拷贝的对象的引用数据内容也会跟着改变。

经典面试题:

  • 手写浅拷贝:
Object.prototype.toDo = "sleep";

let obj = {
  name: "cc",
  age: 18,
  a: "我爱金铲铲",
};

function shallowCopy(obj) {
  let newObj = {};
  for (let key in obj) {
    //for in 循环遍历对象属性
      newObj[key] = obj[key];
  }
  return newObj;
}

console.log(shallowCopy(obj));

关键机制解析
  1. 原型链污染(Object.prototype.toDo = "sleep")

    • 通过修改 Object.prototype,相当于在 JavaScript 世界的"基因库"中插入了一个新特性
    • 所有对象都会通过原型链继承这个属性,就像所有人类都继承了直立行走的能力
    • 这种全局修改会导致意外的属性泄露,是典型的"副作用污染"
  2. for...in 的特性

    • 该循环会遍历对象自身的可枚举属性 + 继承的可枚举属性
    • 就像检查一个人的随身物品时,不仅检查口袋,还检查了家族传承的传家宝
    • 未使用 hasOwnProperty() 检查,导致继承属性被误认为自有属性
  3. 浅拷贝的副作用

    • 浅拷贝像"皮肤移植",只复制表面属性
    • 继承属性会被当作自有属性"移植"到新对象
    • 导致新对象携带了本不该存在的"基因缺陷"

注意在遍历对象属性时不能写成newObj.key而要用[]的形式,不然就是往newObj里面存了一个属性名为key的属性。

深拷贝的两种方法

  • 深拷贝:层层拷贝,新对象不受原对象的影响
  1. JSON.parse(JSON.stringify(obj))
  • 无法识别bigint类型,无法处理函数,无法处理Symbol类型

  • 无法处理循环引用

  1. structuredClone()

1. JSON.parse(JSON.stringify(obj))

JSON.parse(JSON.stringify(obj))自带方法将对象转换为字符串,再将字符串转化为对象,就达到了深拷贝

这样深拷贝的缺陷:

  • 无法识别bigint类型,无法处理函数,无法处理Symbol类型

  • 无法处理循环引用

obj.a=obj.e

obj.e.n=obj.a

2. structuredClone()

缺陷:

没办法复制函数

没办法复制Symbol

没办法复制BigInt类型

面试题: 手写深拷贝



function deepClone(obj) {
  let newObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      //先判断obj[key]类型,如果是原始类型就直接赋值,如果是引用类型
      if (typeof obj[key] === "object" && obj[key] !== null) {
        newObj[key] = deepClone(obj[key]);
      } else {
        newObj[key] = obj[key];
      }
    }
  }

  return newObj;
}
let newObjj = deepClone(obj);
obj.like.a = "你爱我我爱你,蜜雪甜蜜蜜";

console.log(newObjj);

if (typeof obj[key] === "object" && obj[key] !== null) null是特殊的原始类型,typeof的结果是object所以要排除null

深拷贝这里使用了递归,因为逻辑都是一样的,对象里面套对象

结语

走完这趟拷贝探索之旅,相信你已经明白:

  1. 数据存储决定拷贝方式:基本类型直接复制,对象类型复制的是"地址钥匙"

  2. 浅拷贝适合简单场景Object.assign()、解构等 6 招轻松应对单层数据

  3. 深拷贝要层层深入

    • JSON 大法简单但会丢失函数和 Symbol
    • structuredClone()能处理循环引用却搞不定函数
    • 递归深拷贝才是最可靠的终极武器

我的实战建议:日常开发优先用 structuredClone(),但是它是最新推出的函数,部分浏览器无法使用,遇到复杂嵌套对象时,手写递归深拷贝才是王道。记住,当对象里藏着函数、Symbol 或者循环引用时,你的深拷贝函数还需要升级装备——这将是我们的下一个冒险故事!

拷贝的奥秘已在你手中,下次遇到对象套对象的复杂数据,可别再被浅拷贝"坑"啦!

欢迎友友们点赞评论,如有错误欢迎指正