一、参数传递机制
聊到Ja vaScript的函数传参,有个概念是绕不开的:值传递。没错,这门语言采用的确实是值传递,但这里面的“值”,在不同类型的数据上,表现可是大不相同。简单来说,它决定了你在函数内部的操作,会不会“波及”到外部的变量。
对于基本类型,比如数字、字符串,传递进去的是值的“副本”。你在屋里怎么折腾这个副本,外面的原件都完好无损。
而对于引用类型,比如对象、数组,传递进去的则是对象“引用”的副本。这意味着,如果你通过这个引用来修改对象的属性,外面的对象会跟着变;但如果你干脆让这个参数指向一个全新的对象(也就是重新赋值),那么外面的引用依然不动如山。
二、基本类型参数(不可变)
function modifyPrimitive(num) {
num = 100; // 修改的只是本地副本
}
let x = 5;
modifyPrimitive(x);
console.log(x); // 输出:5(原变量纹丝未动)
关键点在这里:参数num拿到的是原始值5的一个精确复制品。函数内部对这个复制品进行任何赋值操作,都如同在平行世界里进行,自然不会影响到外部那个独立的变量x。
三、引用类型参数(可变)
1. 修改属性 → 影响外部
function modifyObject(obj) {
obj.name = 'Bob'; // 通过引用找到“原对象”,并修改其属性
}
let person = { name: 'Alice' };
modifyObject(person);
console.log(person.name); // 输出:Bob(外部对象被成功修改)
这就像是你和朋友共享一份文档的链接。朋友通过链接打开文档,修改了里面的内容,你这边再打开,看到的自然就是修改后的版本。函数内的obj和外部的person,持有的是同一个对象的“地址”,通过这个地址去操作,效果是全局的。
2. 重新赋值参数 → 不影响外部
function replaceObject(obj) {
obj = { name: 'Charlie' }; // 参数“obj”被赋予了全新对象的地址
}
let person = { name: 'Alice' };
replaceObject(person);
console.log(person.name); // 输出:Alice(原对象安然无恙)
这次的区别在于,不是在共享的文档上修改文字,而是朋友直接把给他的那个链接,指向了另一份全新的文档。你手里的链接,指向的依然是原来那份。所以,对参数进行整体的重新赋值(=操作),只是改变了参数本地副本的指向,与外部变量彻底“分手”。
3. 数组示例
function modifyArray(arr) {
arr.push(4); // ✅ 通过引用操作原数组,有效
arr = [5, 6, 7]; // ❌ 重新赋值,仅改变局部参数指向,无效
}
let numbers = [1, 2, 3];
modifyArray(numbers);
console.log(numbers); // 输出:[1, 2, 3, 4]
数组作为对象,逻辑完全一致。push操作是在原有地址上的修改,而arr = ...则是给参数换了一个全新的地址,外部的numbers感知不到这个变化。
四、参数赋值 vs 属性修改
操作类型 对外部的影响 示例
修改基本类型参数 ❌ 不影响外部
function(x) { x = 10; }修改引用类型的属性 ✅ 影响外部
function(obj) { obj.key = 1; }重新赋值引用类型参数 ❌ 不影响外部
function(obj) { obj = {}; }
这张对比表可以帮你快速抓住核心区别。记住,区分是“通过引用修改内容”还是“给引用本身换目标”,是理解这个问题的钥匙。
五、实战分析:forEach中的参数行为
理解了上述原理,再来看Array.prototype.forEach方法里回调函数的行为,就一目了然了。回调函数接收到的item参数,本质就是数组元素(值或引用)的一个副本。
因此:
- 若元素是基本类型,
item就是值的副本,对其重新赋值不影响原数组。 - 若元素是引用类型,
item就是引用的副本,修改其属性会影响原对象,但重新赋值item只改变副本指向。
示例分析
1. 基本类型数组(重新赋值无效)
const arr = [1, 2, 3];
arr.forEach((item) => {
item = item * 10; // 修改的是副本,不影响原数组
});
console.log(arr); // 输出:[1, 2, 3]
原因很清晰:每次迭代,item都是数组中数字1、2、3的独立副本。对副本做数学运算然后赋值,改变的只是这个临时变量,原数组的槽位没有任何写入操作。
2. 引用类型数组(修改属性有效,重新赋值无效)
const arr = [{ value: 1 }, { value: 2 }];
// 情况A:修改属性 → 有效
arr.forEach((item) => {
item.value = item.value * 10; // 通过引用修改原对象
});
console.log(arr); // 输出:[{ value: 10 }, { value: 20 }]
// 情况B:重新赋值 → 无效
arr.forEach((item) => {
item = { value: 100 }; // 创建新对象,副本指向新对象,原数组元素引用不变
});
console.log(arr); // 输出依然是:[{ value: 10 }, { value: 20 }]
这里揭示了两个关键点:
- 修改属性:因为
item和原数组元素指向内存中的同一个对象,所以修改item.value就等于直接修改了那个对象,效果立竿见影。 - 重新赋值:
item = { value: 100 }这条语句,是让参数item这个“引用副本”转而指向一个刚创建出来的全新对象。这就像换了一扇门进去,与原数组元素所指向的那个“房间”彻底断了联系,因此原数组自然不会发生变化。
掌握参数传递是值传递,并分清“修改引用所指的内容”与“更换引用本身”这两种操作,就能从容应对Ja vaScript中函数操作带来的副作用问题,写出更可预测的代码。