深入理解 JavaScript 中的值传递与引用传递机制
本文深入解析 JavaScript 中的值传递与引用传递机制。从堆栈内存模型出发,阐明基本数据类型与引用数据类型的存储差异。通过变量赋值与函数参数传递的实例,揭示 JS 中所有参数传递本质均为值传递的原理,并区分修改属性与重新赋值对原对象的不同影响,帮助开发者规避内存管理与数据共享的常见陷阱。
前言
现代前端开发已告别早期的 jQuery 时代,转向 MVVM 架构、组件化开发与工程化流程,以应对日益复杂的业务逻辑。在此背景下,内存消耗与性能优化成为开发者必须重点关注的核心问题。
本文将从堆内存与栈内存的基础概念入手,解析 JavaScript 变量在内存中的分配方式及其动态变化过程。
初步了解堆栈
JavaScript 的内存空间主要划分为堆(Heap)和栈(Stack)两个区域。代码执行时,解析器会首先判断变量的数据类型,并依据类型将其存储至对应的内存区域中。
如图所示

堆栈和类型的关系
基本数据类型(包括 String、Number、Boolean、Null、Undefined 和 Symbol)的值直接存储在栈区。这些值属于简单数据段,大小确定且内存空间固定,因此可以按值直接访问。
引用数据类型(如 Object 对象)的变量存储在堆区。栈内存中仅保存该对象在堆内存中的引用地址(指针)。通过该地址,程序可以快速定位并访问堆内存中实际存储的对象。由于堆内存对象大小不一,需根据具体情况进行动态配置。
如下代码示例:
var a = 12;
var b = false;
var c = 'string'
var obj = { name: 'sunshine' }
特点
栈区具有空间小、数据类型简单、读写速度快的特点,通常由 JavaScript 引擎自动管理释放。
堆区空间较大,用于存储复杂数据类型,读写速度相对较慢。堆内存中的对象仅在不再被任何变量引用时,才会由垃圾回收机制周期性回收。
理解堆栈与类型的对应关系后,接下来探讨变量在栈区和堆区之间的交互行为。
变量赋值
首先观察基本数据类型的变量传递示例:
let a = 100
let b = a
a = 200
console.log(b) // 100初始状态下,栈中变量 a 的值为 100;随后在栈区创建变量 b,并将 a 的值复制一份赋给 b;最后将 a 重新赋值为 200,此时 b 的值保持为 100 不变。
接着观察引用类型的变量传递示例:
let obj1 = { name: 'a' }
let obj2 = obj1
obj2.name = 'b'
console.log(obj1.name) // b在上述代码中,obj1 和 obj2 指向同一个堆内存对象。将 obj1 赋值给 obj2 时,实际上是将 obj1 在栈中保存的堆内存引用地址复制了一份给 obj2。因此,obj1 和 obj2 的指针均指向堆内存中的同一个对象。
图解如下:

综合案例:
var a = [1, 2, 3, 4]
var c = a[0]
// 这时变量c是基本数据类型,存储在栈内存中;改变栈中的数据不会影响堆中的数据
c = 5
console.log(c) // 5
console.log(a[0]) // 1
let b = a
// b是引用数据类型,栈内存指针和 a一样都指向同一个堆内存,改变数值后,会影响堆中的数据
b[2] = 6
console.log(a[2]) // 6划重点:在 JavaScript 的变量传递中,本质上所有操作均可视为值传递。区别仅在于传递的值是基本数据类型的直接值,还是引用数据类型的内存地址(指针)。由于 JavaScript 不允许直接操作对象的内存空间,必须通过指针(即引用)来访问对象。
因此,即使是复杂数据类型(对象)的赋值操作,其本质依然是值传递。接下来分析不同类型的值在函数参数传递中的具体表现。
参数传递
在 ECMAScript 中,所有函数参数均按值传递。这意味着函数外部的值会被复制到函数内部的参数变量中,行为类似于从一个变量赋值给另一个变量。按值传递时,值被复制到一个局部变量(arguments 对象中的一个槽位);而按引用传递时,内存中的位置被保存在局部变量中,对局部变量的修改会反映到函数外部。
以下示例展示了当参数为基本数据类型时的情况:在 bar 函数内部,参数值被复制了一份,因此函数体内的任何操作都不会影响原参数的实际值。
let foo = 1
const bar = value => {
// var value = foo
value = 2
console.log(value)
}
bar(foo) // 2
console.log(foo) // 1若将函数参数改为引用类型,结果则有所不同:
let foo = { bar: 1}
const func = obj => {
// var obj = foo
obj.bar = 2
console.log(obj.bar)
}
func(foo) // 2
console.log(foo.bar) // 2从上述代码可见,当函数参数为引用类型时,若在函数体内修改该引用类型参数的某个属性,原参数也会随之修改。这是因为函数体内的引用地址指向了原参数所在的堆内存对象。
然而,若在函数体内直接对参数进行重新赋值,情况又会发生变化:
let foo = { bar: 1}
const func = obj => {
// var obj = 2
obj = 2
console.log(obj)
}
func(foo) // 2
console.log(foo) // { bar: 1 }这是因为当对已赋值的变量重新赋值时,该变量将包含新的数据或引用地址。此时函数体内创建了一个新的局部引用,该新引用的任何操作均不会影响原参数的实际值。
若一个对象不再被任何变量引用,JavaScript 引擎的垃圾回收机制将销毁该对象并释放其占用的内存。
小结
- 当函数参数为基本数据类型时,函数体内复制了一份参数值,任何操作均不会影响原参数的实际值。
- 当函数参数为引用类型时,若在函数体内修改该参数的某个属性,原参数将同步被修改。
- 当函数参数为引用类型时,若直接修改该参数的引用地址(重新赋值),相当于在函数体内创建了一个新的局部引用,任何操作均不会影响原参数的实际值。
面试题
- 参数多次赋值问题
function func (person) {
person.age = 25
person = {
age: 50
}
return person
}
var person1 = {
age: 30
}
var person2 = func(person1);
console.log(person1)
console.log(person2)答案:{ age: 25 },{ age: 50 }。在函数内部,person 第一次修改属性相当于复制了 person1 的内存地址;第二次重新赋值 person 变量则创建了一个新的对象。因此,person1 在堆内存中的值被修改,而 person 返回的是新创建的对象值。
- 变量干扰问题
let obj1 = { x: 100, y: 200}
let obj2 = obj1
let x1 = obj1.x
obj2.x = 101
x1 = 102
console.log(obj1)答案:{ x: 101, y: 200 }。x1 是干扰项,因为 obj.x 是原始类型值,修改 x1 不会影响原数据的引用地址或对象结构。
两者的区别就是:
举个例子:
值传递:A 欣赏 B 的房子装修风格并借用,但 A 随后在自己的房子里添加新风格,B 的房子风格保持不变。
引用传递:A 喜欢 B 的房子风格并借用地址,随后 A 添加新风格,并通过 B 的地址强行将 B 的房子风格改为与 A 一致。
总结
本文介绍了 JavaScript 中“值传递”和“引用传递”的核心概念。更多相关内容请搜索本站历史文章或继续阅读后续章节,感谢大家的支持!
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
相关标签:
