💯前言
- JavaScript 是一种基于对象的脚本语言,常用于前端开发。初学者在使用 JavaScript 时,通常会遇到一些关于变量引用和赋值的困惑。本文将详细讨论三种不同的代码场景,结合 JavaScript 的变量引用与内存模型,深入分析为什么这些代码输出会如此不同。希望通过对这些原理的探讨,能够帮助您更好地理解 JavaScript 中的变量引用机制。
💯场景一:直接赋值与重新引用
首先,我们来看以下的代码片段:
var arr = [1, 2, 3];
var newArr = arr;
newArr = [3, 4, 5];
console.log(arr); // 输出是什么?
运行结果:
[1, 2, 3]
为什么结果不是 [3, 4, 5]
?
要理解这个问题,我们需要深入理解 JavaScript 中的变量赋值与引用的区别。
1. 引用与赋值的基本概念
在 JavaScript 中,基本数据类型(如 number
、string
等)是按值传递的,而复杂数据类型(如数组、对象)是按引用传递的。
在代码中,var arr = [1, 2, 3]
创建了一个数组 [1, 2, 3]
,并且将其引用赋值给变量 arr
。此时,arr
保存的是数组在内存中的引用(地址),而不是数组的值本身。
然后,执行 var newArr = arr;
,这意味着 newArr
保存了与 arr
相同的引用。也就是说,newArr
和 arr
都指向了相同的内存地址,这个内存中存储的是数组 [1, 2, 3]
。
当我们执行 newArr = [3, 4, 5];
时,newArr
被重新赋值,指向了一个新的数组 [3, 4, 5]
的内存地址。此时,newArr
不再指向原来的数组 [1, 2, 3]
,而是指向了一个全新的数组。而 arr
依然保持指向原始数组的引用,因此打印 arr
时结果仍然是 [1, 2, 3]
。
2. 图示分析
-
初始状态:
arr
和newArr
都指向[1, 2, 3]
。
-
重新赋值后:
newArr
指向新的数组[3, 4, 5]
。arr
依然指向原数组[1, 2, 3]
。
关键总结
在 JavaScript 中,给一个变量赋予一个新的数组时,并不会改变原来的数组,而是创建了一个新的引用。如果希望改变所有引用同一数组的变量,那么需要对数组本身进行修改,而不是重新赋值。
💯场景二:引用指向的变化
接下来看第二个代码片段:
var arr = [1, 2, 3];
var newArr = arr;
arr = [4, 5, 6];
console.log(newArr); // 输出是什么?
运行结果:
[1, 2, 3]
为什么结果还是 [1, 2, 3]
?
在这个场景中,我们遇到了类似的问题。在执行 var newArr = arr;
之后,newArr
和 arr
都指向同一个数组 [1, 2, 3]
。但是,当执行 arr = [4, 5, 6];
时,arr
被重新赋值,指向了一个新的数组 [4, 5, 6]
。
需要注意的是,这种赋值不会影响到 newArr
,因为 newArr
依旧保持指向原来的数组 [1, 2, 3]
。简单来说,arr
重新指向了一个新的对象,而 newArr
还在指向原来的数组。
对象引用与原始数据的区别
在 JavaScript 中,对象、数组等复杂数据类型的变量并不直接保存数据的值,而是保存引用。当我们对变量重新赋值时,我们只是改变了它指向的内存地址,而原来的引用仍然有效。这也是为什么在打印 newArr
时,它依旧指向 [1, 2, 3]
。
重新赋值与直接修改的差异
如果我们希望改变 newArr
也能看到新数组的变化,就不能直接给 arr
重新赋值,而是需要修改数组本身的内容,比如:
arr.push(4);
这样,arr
和 newArr
都会看到新的内容。
💯场景三:修改数组内容
最后,我们来看第三个代码片段:
var arr = [1, 2, 3];
var newArr = arr;
arr[2] = 6;
console.log(newArr); // 输出是什么?
运行结果:
[1, 2, 6]
为什么结果变成了 [1, 2, 6]
?
在这里,我们需要理解的一个重要概念是“修改数组的内容”和“重新赋值”的区别。
var arr = [1, 2, 3];
创建了一个数组,并将其引用赋值给arr
。var newArr = arr;
将arr
的引用赋值给newArr
,此时arr
和newArr
都指向同一个数组。arr[2] = 6;
直接修改了数组的第三个元素。
由于 arr
和 newArr
都指向相同的数组,这意味着对数组内容的任何更改对这两个变量都是可见的。因此,当 arr[2]
被修改为 6
时,newArr
看到的也是修改后的数组 [1, 2, 6]
。
JavaScript 中的内存共享
在 JavaScript 中,数组和对象是通过引用来传递的。当多个变量引用同一个数组时,修改这个数组的内容将影响到所有引用该数组的变量。这种行为称为内存共享。
要理解内存共享,可以将数组或对象看作是存在于某个位置的数据块,而变量是指向这个数据块的“指针”。当我们通过一个变量修改数据块时,所有引用这个数据块的变量都会反映出相应的变化。
💯深入理解 JavaScript 的内存模型与赋值行为
为了更好地理解上述三种情况,我们还需要进一步了解 JavaScript 的内存管理和变量赋值行为。
1. JavaScript 中的值类型和引用类型
JavaScript 中的数据类型分为两类:
- 基本数据类型(值类型):包括
Number
、String
、Boolean
、Null
、Undefined
、Symbol
、BigInt
。这些类型的数据是按值传递的,这意味着每个变量都存储数据的副本。 - 引用数据类型:包括
Object
、Array
、Function
等。这些类型的数据是按引用传递的,变量保存的是对象的内存地址,而不是对象本身。
对于基本数据类型,变量赋值是直接复制值的副本,因此两个变量之间不会互相影响。对于引用类型,变量保存的是对象在内存中的地址,两个引用指向相同的地址意味着它们共享相同的内存内容。
2. 内存分配与垃圾回收
JavaScript 的内存分为两种主要区域:
- 栈内存(Stack Memory):用于存储基本类型的值和引用类型的引用。
- 堆内存(Heap Memory):用于存储引用类型的实际内容(对象、数组等)。
当执行赋值操作 var newArr = arr
时,newArr
和 arr
都指向堆内存中的同一个数组对象,因此对数组内容的修改对这两个变量来说是可见的。而当重新赋值 arr = [4, 5, 6]
时,arr
被重新赋予了一个新的引用,因此它和 newArr
分道扬镳。
💯如何避免引用带来的问题
在实际开发中,共享引用数据类型可能会带来一些不可预见的副作用,因此有时我们希望克隆数组或对象,以避免修改对其他变量产生影响。
1. 浅拷贝与深拷贝
浅拷贝 只复制对象的第一层引用,而 深拷贝 会递归复制所有嵌套的对象和数组。
-
浅拷贝的方法:使用
Object.assign()
或展开运算符...
。var arr = [1, 2, 3]; var shallowCopy = [...arr]; // 浅拷贝 shallowCopy[0] = 9; console.log(arr); // [1, 2, 3] console.log(shallowCopy); // [9, 2, 3]
-
深拷贝的方法:使用
JSON.parse(JSON.stringify(obj))
(这种方式有局限性,无法拷贝函数和某些特殊对象)。var obj = { a: 1, b: { c: 2 } }; var deepCopy = JSON.parse(JSON.stringify(obj)); deepCopy.b.c = 9; console.log(obj.b.c); // 2 console.log(deepCopy.b.c); // 9
2. 使用 Object.assign
或 lodash.cloneDeep
Object.assign()
可以用来实现对象的浅拷贝,而 lodash
库提供了一个更强大的深拷贝方法 _.cloneDeep()
,可以递归地复制嵌套的对象和数组。
💯小结
在 JavaScript 中,理解变量赋值、引用以及内存模型对于掌握语言的行为至关重要。在本文中,我们详细探讨了三种代码场景,并通过对比分析深入理解了以下几点:
- 变量赋值与引用:赋值为引用数据类型时,变量保存的是内存地址,而不是数据本身。因此,重新赋值并不会影响其他引用该数据的变量。
- 内存模型:JavaScript 中,栈内存用于存储基本类型和引用地址,而堆内存用于存储复杂对象的内容。对引用对象的操作会影响到所有指向该内存地址的变量。
- 修改数组内容与重新赋值:直接修改数组的内容会影响所有引用该数组的变量,而重新赋值则会让变量指向一个新的对象,不影响其他引用。