JavaScript 传值机制全解:为什么对象是"引用按值传递"——jstips 44 深度解读

原创2026-10-07 21:28:362,303 阅读
文章标签:教程

JavaScript 传值机制全解:为什么对象是"引用按值传递"——jstips #44 深度解读

导读:本文以 jstips 仓库第 44 期 tip《了解传值机制》为骨架,剖析 JavaScript 中"按值传递"与"按引用传递"的经典争论。通过两个对照实例,你将彻底理解"对象传的是引用的值"(即 call-by-sharing)这一本质,并掌握它在实际编码中如何影响函数内外数据的一致性,从而写出行为可预期的代码。

一、先亮结论:JavaScript 在技术上"只按值传递"

在讨论参数传递机制时,计算机科学里有两个经典术语:

  • 按值传递(pass-by-value):调用函数时,把实参的值复制一份交给形参,函数内对形参的任何改动都不会影响外部变量。
  • 按引用传递(pass-by-reference):把实参的内存地址交给形参,函数内对形参的操作会直接作用于外部变量本身。

JavaScript 的传参行为,用任何一个术语去"生搬硬套"都会出错。正如本 tip 开头所说:

JavaScript 技术上只按值传递(pass-by-value);对对象(引用)类型而言,传递的是引用值本身,而引用值也是按值传递的。

这句话听起来绕,但用下面两个实例可以完全讲清楚。原始讨论参见 中文版文档 与 英文原版文档。

二、实例 1:函数内重新赋值,外部对象纹丝不动

var me = {					// 1
	'partOf' : 'A Team'
};

function myTeam(me) {		// 2

	me = {					// 3
		'belongsTo' : 'A Group'
	};
}

myTeam(me);
console.log(me);			// 4  : {'partOf' : 'A Team'}

逐行拆解:

  1. 第 1 行:me 是一个对象字面量,其值是一个指向堆内存中对象 {partOf: 'A Team'} 的引用。
  2. 第 2 行:调用 myTeam(me) 时,JavaScript 把 me 这个引用值复制了一份传给函数内部的形参。此时函数内外各有一个引用变量,但它们指向同一个对象。这里内外都叫 me 很有迷惑性——它给人"只有一个引用"的错觉,实际上它们是两个相互独立的引用变量。
  3. 第 3 行:函数内部把形参 me 重新赋值为一个全新的对象 {belongsTo: 'A Group'}。这只是在函数作用域内改写了这个引用的值(让它指向新对象),完全没有触碰外部变量 me 所持有的引用。
  4. 第 4 行:因此外部打印的仍是原始对象:{'partOf' : 'A Team'}。

结论:引用变量本身是独立的;函数内给形参"换个新对象",影响不了外部。

三、实例 2:函数内修改属性,外部对象同步变化

var me = {					// 1
	'partOf' : 'A Team'
};

function myGroup(me) { 		// 2
	me.partOf = 'A Group';  // 3
}

myGroup(me);
console.log(me);			// 4  : {'partOf' : 'A Group'}

逐行拆解:

  1. 第 1 行:同样的对象字面量,外部持有其引用。
  2. 第 2 行:myGroup(me) 同样把引用值复制进函数。
  3. 第 3 行:关键区别来了——这里没有给形参重新赋值,而是通过形参持有的引用访问并修改了对象上的属性 me.partOf。
  4. 第 4 行:因为内外两个引用仍然指向同一个对象,所以通过函数内引用修改的属性,外部打印时同样能看到:{'partOf' : 'A Group'}。

结论:修改对象内容(而非替换引用)会"穿透"函数边界,直接影响调用方看到的对象。

四、那 JavaScript 是按引用传递吗?——不,是"引用按值传递"

实例 2 很容易让人得出"JavaScript 对象按引用传递"的结论,但本 tip 明确否定了这一点:

请记住:如果是对象的话,JavaScript 将引用按值传递。

两种实例放在一起对比,本质一目了然:

行为 实例 1(重新赋值) 实例 2(修改属性)
函数内对形参做什么 me = {...} 替换引用 me.partOf = ... 修改对象内容
外部对象是否受影响 否,外部引用仍指向原对象 是,内外引用指向同一对象
原因 形参只是引用的副本,改副本不影响原件 副本与原件指向同一堆对象,改对象内容双方都可见

如果真的是"按引用传递",实例 1 里函数内把形参指向新对象后,外部变量 me 也应该跟着变成新对象——但它没有。这正是"引用按值传递"的铁证:传进函数的不是对象本身,也不是能改写外部变量的"引用变量",而只是引用值的副本。

由于引用值被共享(副本和原件指向同一目标),但又确实是通过"拷贝"进入函数,有人把这种机制称为 call-by-sharing(共享调用 / 按共享传递),这是对 JavaScript 传参机制更精确的描述。

五、从内存模型看本质:引用是什么,拷贝的是什么

要彻底消化上面的结论,可以借助简化的内存模型来理解:

  • 原始类型(primitive):number、string、boolean、null、undefined、symbol、bigint 直接存值。传参时拷贝的就是值本身,函数内任何修改都不会外泄。这是纯粹的 pass-by-value。
  • 对象类型(object/reference):变量里存的不是对象数据,而是一个"指向堆中对象的引用"(可类比为门牌号)。传参时拷贝的是门牌号,函数内外两个变量持有一模一样的门牌号,都通向同一间房。

于是"房间里的家具"(对象属性)可以被任何持有门牌号的人改动;但"把门牌号换成另一间房的门牌号"(重新赋值形参),只改变持号人自己,别人手里的门牌号不受影响。

这正是本 tip 两个实例在内存层面的唯一差别:实例 1 换了门牌号,实例 2 进了房间搬家具。

六、仓库内相关 tips 的互相印证

jstips 仓库中还有其他若干 tips 与传参机制互为补充,可以加深理解:

1. 传参副本的代价:arguments 泄漏会杀死 V8 优化

既然参数以"值/引用的副本"进入函数,那么函数内部对 arguments 对象的使用方式也会影响引擎优化。在 tip 31:避免把 arguments 传给其他函数 中明确指出:把 arguments 直接传给其他函数(称为 leaking arguments)会导致 Chrome 与 Node 使用的 V8 引擎跳过该函数的优化,显著拖慢性能。该 tip 给出的替代方案是显式拷贝:

var args = new Array(arguments.length);
for(var i = 0; i < args.length; ++i) {
  args[i] = arguments[i];
}

这从另一个侧面印证了:在 JavaScript 中"拷贝值"是绕不开的日常操作,理解拷贝的是值还是引用,直接关系到性能与正确性。

2. 共享引用的风险:为什么需要不可变对象

正因为对象按共享传递、函数内修改属性会外泄,仓库中多个 tips 都在讨论如何规避这种副作用:

当你把对象传给函数时,如果不想让它被改,Object.freeze() 是最简单直接的防御手段;这恰恰是对"共享引用"这一特性的反向利用。

3. 传参配合闭包:把"值"与"引用"用到位

tip 16:向回调函数传参 展示了另一种传参思路:通过闭包捕获变量,或借助 bind 预先绑定参数。闭包捕获的是变量与引用的"可达性",与本文的传值机制配合使用,可以写出既灵活又行为确定的回调代码。

七、实战建议:把传值机制变成你的心智模型

把本 tip 的核心结论落到日常编码中,可以总结出几条可直接使用的准则:

  1. 判断一段代码会不会"外泄"改动,看它操作的是引用还是对象内容:
    • param = newValue → 只影响函数内;
    • param.prop = newValue / param.method() → 会影响外部共享对象。
  2. 不想让函数改动传入对象:要么传副本(浅拷贝/深拷贝),要么对入参做 Object.freeze() 防护,或设计成纯函数(返回新对象而非就地修改)。
  3. 警惕"改形参"与"改对象"混用:在大型项目中,同一个对象被多处函数共享时,任何一处"改属性"都可能产生跨模块的连锁影响;这是按共享传递机制下最常见的隐式耦合来源。
  4. 面试与基础复盘时用两个实例自测:能否在不运行代码的情况下,准确说出上面两个 console.log 的输出?能说对,说明你对"引用按值传递"的理解已经到位。

八、结语

JavaScript 的传参机制可以一句话概括:原始类型按值传递,对象类型"引用按值传递",本质是 call-by-sharing。这一机制决定了"重新赋值形参"与"修改对象属性"在行为上的天壤之别,也解释了为什么不可变数据结构、纯函数等范式在 JS 生态中如此重要。掌握它,你就能准确预测函数调用的一切副作用。

延伸阅读:本文整理自 jstips 仓库第 44 期,原始多语言版本可在 中文版、英文版、繁体版 与 西语版 中查阅;更多同类技巧可浏览仓库 README.md 中的完整 tip 列表。

登录后查看全文
jstips