news 2026/5/23 18:53:49

一次搞懂!JavaScript中的引用赋值、浅拷贝和深拷贝

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一次搞懂!JavaScript中的引用赋值、浅拷贝和深拷贝

如果你经常搞混 深浅拷贝 和 引用赋值,总是记不住它们有什么区别,在实际开发中总是踩坑——比如不小心修改了原始数据、或者拷贝不彻底导致奇怪的 bug——那么恭喜你,这篇文章就是为你写的!我会用最直白的语言、清晰的图示和大量实际代码示例,帮你一次性彻底搞懂!在深入探讨拷贝机制之前,我们需要先了解 JavaScript 的数据类型分类和内存存储机制的基础概念

一、基础概念铺垫

1. JavaScript 数据类型分类

  • 基本数据类型:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol、BigInt。
  • 引用数据类型:对象(Object)、数组(Array)、函数(Function),还有两个特殊的对象:正则(RegExp)、日期(Date)、正则表达式、Map、Set、和其他内置对象(比如Promise、Error等)

2. 内存存储机制核心原理

JS 引擎将内存划分为栈内存(Stack)堆内存(Heap),不同类型的数据会被分配到不同的内存区域:嵌套引用依旧遵循下列规则

特性栈内存 (Stack)堆内存 (Heap)
存储内容基本数据类型值、引用类型的指针引用数据类型的实际内容
数据结构后进先出 (LIFO)动态分配的树/图结构
分配方式连续内存,自动分配随机内存,动态分配
访问速度极快(直接CPU访问)较慢(通过指针间接访问)
大小限制小(通常1-8MB)大(可达GB级)
生命周期函数/块作用域结束自动释放由垃圾回收器(GC)管理

如上图所示,两种数据类型的内存访问流程如下所示

  • 基本数据类型的变量直接从栈内存中获取数据
  • 引用数据类型的变量访问过程:
    1. 读取栈内存中的指针(地址)
    2. 通过指针找到堆内存中存储的实际数据

二、不同类型的拷贝行为

1. 基本数据类型的拷贝

基本数据类型的拷贝非常简单,由于它们直接存储在栈内存中,拷贝时会直接复制值本身,不存在引用关系。

let a = 10; let b = a; // 直接复制值 b = 20; console.log(a); // 10(不受 b 修改影响) console.log(b); // 20

2. 引用数据类型的拷贝

方式一:引用赋值(非拷贝)

引用数据类型在赋值时,默认是引用赋值(即复制指针地址),而非复制实际内容。这意味着两个变量会指向堆内存中的同一个对象。修改其中一个另一个会受影响。

let a = [1, 2, 5]; let b = a; // 引用赋值(复制指针) a[1] = 4; // 修改 a 指向的数组 console.log(a); // [1, 4, 5] console.log(b); // [1, 4, 5](b 也受影响) console.log(a === b); // true(指向同一个对象)

方式二: 浅拷贝

浅拷贝是针对引用类型的拷贝方式,它会创建一个新对象,但只复制对象的第一层属性。其规则是:

  • 基本类型属性:直接复制值
  • 引用类型属性:仅复制指针(不复制指向的对象本身)

示例1:对数组a = [1, 2, [3, 4], 5]进行浅拷贝得到b后:

  • b[0]b[1]b[3]是基本类型,修改它们不会影响a
  • b[2]是引用类型(数组),修改b[2]会同时影响a[2],因为它们指向同一个子数组
const a = [1, 2, [3, 4], 5]; const b = [...a]; // 浅拷贝 a[0] = 100; console.log(b[0]); // 1(不受影响,基本类型独立) a[2][1] = 400; // 修改子数组元素 console.log(b[2][1]); // 400(受影响,共享子数组引用)

示例2:

// 原始对象 const a = { name: "alice", // 基本类型(栈内存存储值) profile: { // 引用类型(堆内存存储对象,栈内存存储指针) age: 25, city: "beijing" } }; // 使用扩展运算符进行浅拷贝 const b = { ...a }; // 修改浅拷贝对象 b.name = "bob"; b.profile.age = 30; b.profile.city = "shanghai"; // 查看结果 console.log("原始对象 a.name:", a.name); // 输出:"alice"(基本类型值独立,不受影响) console.log("原始对象 a.profile.age:", a.profile.age); // 输出:30(引用类型共享堆内存,被修改) console.log("原始对象 a.profile.city:", a.profile.city); // 输出:"shanghai"(引用类型共享堆内存,被修改) console.log("a.profile === b.profile:", a.profile === b.profile); // 输出:true(两者指向堆中同一个对象)

常见的浅拷贝方法

  1. 扩展运算符(推荐)
const obj = { a: 1, b: { c: 2 } }; const shallowCopy = { ...obj };
  1. Object.assign()
const obj = { a: 1, b: { c: 2 } }; const shallowCopy = Object.assign({}, obj);
  1. 数组的 slice()、concat() 方法
const arr = [1, 2, { a: 3 }]; const shallowCopy1 = arr.slice(); const shallowCopy2 = [].concat(arr);
  1. Array.from()
const arr = [1, 2, { a: 3 }]; const shallowCopy = Array.from(arr);

方式三、深拷贝

深拷贝会创建一个全新的对象,完全复制原始对象的所有层级属性,包括嵌套的引用类型,使得新旧对象完全独立。

实现方式 1:JSON 方法(最常用但有局限)

// 原始对象 const a = { name: "alice", // 基本类型(栈内存存储值) profile: { // 引用类型(堆内存存储对象) age: 25, city: "beijing" } }; // 实现深拷贝 const deepCopy = JSON.parse(JSON.stringify(a)); // 修改深拷贝对象的属性 deepCopy.name = "bob"; // 修改基本类型 deepCopy.profile.age = 30; // 修改嵌套引用类型 deepCopy.profile.city = "shanghai"; // 查看结果对比 console.log("原始对象 a.name:", a.name); // 输出:"alice"(基本类型不受影响) console.log("原始对象 a.profile.age:", a.profile.age); // 输出:25(嵌套引用类型也不受影响) console.log("原始对象 a.profile.city:", a.profile.city); // 输出:"beijing"(嵌套引用类型完全独立) console.log("a.profile === deepCopy.profile:", a.profile === deepCopy.profile); // 输出:false(两者指向堆中不同对象)

注意限制:

  • 无法复制函数、undefined、Symbol
  • 日期对象会被转换为字符串
  • 无法处理循环引用
  • 会丢失原型链信息

实现方式 2:递归实现(自定义深拷贝函数)

由于递归实现较为复杂,这里不展开详细代码,但基本原理是遍历对象的所有属性,对引用类型属性递归调用拷贝函数,直到所有层级都被复制。

三、深浅拷贝对比总结

类型引用类型内存地址第一层修改第二层修改
引用赋值引用复制相同相互影响相互影响
浅拷贝仅第一层值复制,嵌套层引用复制不同独立相互影响
深拷贝完全复制不同独立独立

深浅拷贝的核心区别在于对嵌套引用类型的处理方式,这直接决定了拷贝后对象的独立性:

  • 引用赋值:本质上不是拷贝,只是复制了对象的引用指针。两个变量共享同一块堆内存,任何层级的修改都会相互影响,内存地址相同。
  • 浅拷贝:创建新的内存地址存储对象,但仅对第一层属性进行值复制。对于基本类型属性,修改后彼此独立;但对于嵌套的引用类型属性,仍共享原始引用,修改会相互影响。
  • 深拷贝:完全创建新的对象,递归复制所有层级的属性(包括嵌套引用类型)。新旧对象拥有完全独立的内存空间,任何层级的修改都不会相互影响,内存地址不同。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/23 10:24:07

Fiddler 无法抓包手机 https 报文的解决方案来啦!!

解决手机https无法抓包的问题 当你测试App的时候,想要通过Fiddler/Charles等工具抓包看下https请求的数据情况,发现大部分的App都提示网络异常/无数据等等信息 这时候怎么解决呢? 以软件测试面试提刷题APP为例: Fiddler上的显示…

作者头像 李华
网站建设 2026/5/23 16:51:49

终极代码生成解决方案:OpenReasoning-Nemotron-14B快速部署完整指南

终极代码生成解决方案:OpenReasoning-Nemotron-14B快速部署完整指南 【免费下载链接】OpenReasoning-Nemotron-14B 项目地址: https://ai.gitcode.com/hf_mirrors/nvidia/OpenReasoning-Nemotron-14B 在当今快速发展的软件开发领域,程序员们经常…

作者头像 李华
网站建设 2026/5/23 14:17:25

react中的使用useReducer和Context实现todolist

store.ts - 类型定义 初始状态import { nanoid } from nanoid// 定义单个 Todo 的类型(约束结构:id标题) export type TodoType {id: stringtitle: string }// 初始状态:一个包含2个Todo的数组,用nanoid生成唯一id c…

作者头像 李华
网站建设 2026/5/22 20:56:40

AppPolice:让你的Mac告别卡顿的终极CPU管理神器

AppPolice:让你的Mac告别卡顿的终极CPU管理神器 【免费下载链接】AppPolice MacOS app for quickly limiting CPU usage by running applications 项目地址: https://gitcode.com/gh_mirrors/ap/AppPolice 还在为Mac电脑突然变慢而烦恼吗?当你正在…

作者头像 李华
网站建设 2026/5/22 18:20:31

浅谈web性能测试

什么是性能测试? web性能应该注意些什么? 性能测试,简而言之就是模仿用户对一个系统进行大批量的操作,得出系统各项性能指标和性能瓶颈,并从中发现存在的问题,通过多方协助调优的过程。而web端的性能测试…

作者头像 李华
网站建设 2026/5/23 14:16:43

《UNIX高级环境编程》 第七章 进程环境 读书笔记

一、main函数 C程序总是从main函数开始执行,main函数的原型是: int main(int argc,char *argv[]); 正如前面提到的,argc是命令行参数的数目,argv是指向个参数的指针组成的数组。 当内核执行C程序时,在调用main前先调用…

作者头像 李华