一次彻底搞懂JavaScript中的引用赋值、浅拷贝和深拷贝
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">前言</a></li><li><a href="#_label1">一、基础概念铺垫</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_0">1. JavaScript 数据类型分类</a></li><li><a href="#_lab2_1_1">2. 内存存储机制核心原理</a></li></ul><li><a href="#_label2">二、不同类型的拷贝行为</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_2">1. 基本数据类型的拷贝</a></li><li><a href="#_lab2_2_3">2. 引用数据类型的拷贝</a></li><ul class="third_class_ul"><li><a href="#_label3_2_3_0">方式一:引用赋值(非拷贝)</a></li><li><a href="#_label3_2_3_1">方式二: 浅拷贝</a></li><li><a href="#_label3_2_3_2">方式三、深拷贝</a></li></ul></ul><li><a href="#_label3">三、深浅拷贝对比总结</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>前言</h2><p>如果你经常搞混 深浅拷贝 和 引用赋值,总是记不住它们有什么区别,在实际开发中总是踩坑——比如不小心修改了原始数据、或者拷贝不彻底导致奇怪的 bug——那么恭喜你,这篇文章就是为你写的!我会用最直白的语言、清晰的图示和大量实际代码示例,帮你一次性彻底搞懂!在深入探讨拷贝机制之前,我们需要先了解 JavaScript 的数据类型分类和内存存储机制的基础概念</p>
<p class="maodian"><a name="_label1"></a></p><h2>一、基础概念铺垫</h2>
<p class="maodian"><a name="_lab2_1_0"></a></p><h3>1. JavaScript 数据类型分类</h3>
<ul><li><strong>基本数据类型</strong>:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol、BigInt。</li><li><strong>引用数据类型</strong>:对象(Object)、数组(Array)、函数(Function),还有两个特殊的对象:正则(RegExp)、日期(Date)、正则表达式、Map、Set、和其他内置对象(比如Promise、Error等)</li></ul>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>2. 内存存储机制核心原理</h3>
<p>JS 引擎将内存划分为<strong>栈内存(Stack)</strong> 和<strong>堆内存(Heap)</strong>,不同类型的数据会被分配到不同的内存区域:嵌套引用依旧遵循下列规则</p>
<table><thead><tr><th>特性</th><th>栈内存 (Stack)</th><th>堆内存 (Heap)</th></tr></thead><tbody><tr><td><strong>存储内容</strong></td><td>基本数据类型值、引用类型的指针</td><td>引用数据类型的实际内容</td></tr><tr><td><strong>数据结构</strong></td><td>后进先出 (LIFO)</td><td>动态分配的树/图结构</td></tr><tr><td><strong>分配方式</strong></td><td>连续内存,自动分配</td><td>随机内存,动态分配</td></tr><tr><td><strong>访问速度</strong></td><td>极快(直接CPU访问)</td><td>较慢(通过指针间接访问)</td></tr><tr><td><strong>大小限制</strong></td><td>小(通常1-8MB)</td><td>大(可达GB级)</td></tr><tr><td><strong>生命周期</strong></td><td>函数/块作用域结束自动释放</td><td>由垃圾回收器(GC)管理</td></tr></tbody></table>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025122610594035.png" /></p>
<p><strong>如上图所示,两种数据类型的内存访问流程如下所示</strong>:</p>
<ul><li>基本数据类型的变量直接从栈内存中获取数据</li><li>引用数据类型的变量访问过程:<ol><li>读取栈内存中的指针(地址)</li><li>通过指针找到堆内存中存储的实际数据</li></ol></li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>二、不同类型的拷贝行为</h2>
<p class="maodian"><a name="_lab2_2_2"></a></p><h3>1. 基本数据类型的拷贝</h3>
<p>基本数据类型的拷贝非常简单,由于它们直接存储在栈内存中,拷贝时会直接复制值本身,不存在引用关系。</p>
<div class="jb51code"><pre class="brush:js;">let a = 10;
let b = a; // 直接复制值
b = 20;
console.log(a); // 10(不受 b 修改影响)
console.log(b); // 20
</pre></div>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>2. 引用数据类型的拷贝</h3>
<p class="maodian"><a name="_label3_2_3_0"></a></p><h4>方式一:引用赋值(非拷贝)</h4>
<p>引用数据类型在赋值时,默认是<strong>引用赋值</strong>(即复制指针地址),而非复制实际内容。这意味着两个变量会指向堆内存中的同一个对象。修改其中一个另一个会受影响。</p>
<div class="jb51code"><pre class="brush:js;">let a = ;
let b = a; // 引用赋值(复制指针)
a = 4; // 修改 a 指向的数组
console.log(a); //
console.log(b); // (b 也受影响)
console.log(a === b); // true(指向同一个对象)
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025122610594040.png" /></p>
<p class="maodian"><a name="_label3_2_3_1"></a></p><h4>方式二: 浅拷贝</h4>
<p>浅拷贝是针对引用类型的拷贝方式,它会创建一个新对象,但只复制对象的第一层属性。其规则是:</p>
<ul><li>对<strong>基本类型属性</strong>:直接复制值</li><li>对<strong>引用类型属性</strong>:仅复制指针(不复制指向的对象本身)</li></ul>
<p><strong>示例1</strong>:对数组 <code>a = , 5]</code> 进行浅拷贝得到 <code>b</code> 后:</p>
<ul><li><code>b</code>、<code>b</code>、<code>b</code> 是基本类型,修改它们不会影响 <code>a</code></li><li><code>b</code> 是引用类型(数组),修改 <code>b</code> 会同时影响 <code>a</code>,因为它们指向同一个子数组</li><li><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025122610594055.png" /></li></ul>
<div class="jb51code"><pre class="brush:js;">const a = , 5];
const b = [...a]; // 浅拷贝
a = 100;
console.log(b); // 1(不受影响,基本类型独立)
a = 400; // 修改子数组元素
console.log(b); // 400(受影响,共享子数组引用)
</pre></div>
<p><strong>示例2:</strong></p>
<div class="jb51code"><pre class="brush:js;">// 原始对象
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(两者指向堆中同一个对象)
</pre></div>
<p><strong>常见的浅拷贝方法</strong>:</p>
<ol><li><strong>扩展运算符(推荐)</strong></li></ol>
<div class="jb51code"><pre class="brush:js;">const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
</pre></div>
<ol start="2"><li><strong>Object.assign()</strong></li></ol>
<div class="jb51code"><pre class="brush:js;">const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
</pre></div>
<ol start="3"><li><strong>数组的 slice()、concat() 方法</strong></li></ol>
<div class="jb51code"><pre class="brush:js;">const arr = ;
const shallowCopy1 = arr.slice();
const shallowCopy2 = [].concat(arr);
</pre></div>
<ol start="4"><li><strong>Array.from()</strong></li></ol>
<div class="jb51code"><pre class="brush:js;">const arr = ;
const shallowCopy = Array.from(arr);
</pre></div>
<p class="maodian"><a name="_label3_2_3_2"></a></p><h4>方式三、深拷贝</h4>
<p>深拷贝会创建一个全新的对象,完全复制原始对象的所有层级属性,包括嵌套的引用类型,使得新旧对象完全独立。</p>
<p><strong>实现方式 1:JSON 方法</strong>(最常用但有局限)</p>
<div class="jb51code"><pre class="brush:js;">// 原始对象
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(两者指向堆中不同对象)
</pre></div>
<p><strong>注意限制:</strong></p>
<ul><li>无法复制函数、undefined、Symbol</li><li>日期对象会被转换为字符串</li><li>无法处理循环引用</li><li>会丢失原型链信息</li></ul>
<p><strong>实现方式 2:递归实现</strong>(自定义深拷贝函数)</p>
<p>由于递归实现较为复杂,这里不展开详细代码,但基本原理是遍历对象的所有属性,对引用类型属性递归调用拷贝函数,直到所有层级都被复制。</p>
<p class="maodian"><a name="_label3"></a></p><h2>三、深浅拷贝对比总结</h2>
<table><thead><tr><th>类型</th><th>引用类型</th><th>内存地址</th><th>第一层修改</th><th>第二层修改</th></tr></thead><tbody><tr><td>引用赋值</td><td>引用复制</td><td>相同</td><td>相互影响</td><td>相互影响</td></tr><tr><td>浅拷贝</td><td>仅第一层值复制,嵌套层引用复制</td><td>不同</td><td>独立</td><td>相互影响</td></tr><tr><td>深拷贝</td><td>完全复制</td><td>不同</td><td>独立</td><td>独立</td></tr></tbody></table>
<p>深浅拷贝的核心区别在于对嵌套引用类型的处理方式,这直接决定了拷贝后对象的独立性:</p>
<ul><li><strong>引用赋值</strong>:本质上不是拷贝,只是复制了对象的引用指针。两个变量共享同一块堆内存,任何层级的修改都会相互影响,内存地址相同。</li><li><strong>浅拷贝</strong>:创建新的内存地址存储对象,但仅对第一层属性进行值复制。对于基本类型属性,修改后彼此独立;但对于嵌套的引用类型属性,仍共享原始引用,修改会相互影响。</li><li><strong>深拷贝</strong>:完全创建新的对象,递归复制所有层级的属性(包括嵌套引用类型)。新旧对象拥有完全独立的内存空间,任何层级的修改都不会相互影响,内存地址不同。</li></ul>
頁:
[1]