JavaScript 传参陷阱:值传递 vs 引用传递

244 阅读4分钟

在 JavaScript 编程中,理解参数传递机制是基础且关键的一环。许多初学者在处理函数参数时会遇到困惑:为什么有些情况下函数内部的修改会影响外部变量,而有些情况下却不会?这背后的核心原因在于 JavaScript 对不同数据类型采用了不同的参数传递方式。

一、数据类型基础

JavaScript 中的数据类型分为两大类:

  1. 基本数据类型(值类型):

    • NumberStringBooleanNullUndefinedSymbolBigInt
    • 直接存储在栈内存中,值不可变
  2. 引用数据类型

    • Object(包括数组、函数、日期等)
    • 值存储在堆内存中,栈内存存储指向堆内存的引用地址

二、两个参数传递的核心区别

JavaScript 中所有参数传递都是值传递,但由于数据类型的不同,表现出两种行为:

  1. 基本数据类型传递的是值的副本
  2. 引用数据类型传递的是引用地址的副本

三、基本数据类型的参数传递

当传递基本数据类型时,函数接收的是原始值的一个副本,而非值本身。因此,函数内部对参数的修改不会影响外部变量。

示例代码

function modifyValue(num) {
    num = num + 1;
    console.log('函数内部的 num:', num);
}

let res = 0;
modifyValue(res);
console.log('函数外部的 res:', res);

image.png

内存分析

  1. 变量 res 在栈内存中存储值 0
  2. 调用 modifyValue(res) 时,将 res 的值复制一份传递给参数 num
  3. 参数 num 在函数内部有自己独立的内存空间
  4. 修改 num 的值不会影响原始的 res

四、引用数据类型的参数传递

当传递引用数据类型时,函数接收的是引用地址的副本。虽然地址被复制,但它们指向同一个堆内存对象。因此,函数内部对参数的修改会影响外部变量。

示例代码

function modifyArray(arr) {
    arr.push(4);
    console.log('函数内部的 arr:', arr);
}

let myArray = [1, 2, 3];
modifyArray(myArray);
console.log('函数外部的 myArray:', myArray);

image.png

内存分析

  1. 变量 myArray 在栈内存中存储引用地址(如 0x123),指向堆内存中的数组 [1, 2, 3]
  2. 调用 modifyArray(myArray) 时,将引用地址 0x123 复制一份传递给参数 arr
  3. 参数 arr 和原始 myArray 指向同一个堆内存对象
  4. 修改 arr 会直接影响堆内存中的对象,因此外部的 myArray 也会改变

五、常见误解与深度辨析

1. "引用传递" 的误解

许多人认为 JavaScript 中引用数据类型是 "引用传递",这是不准确的。实际上:

  • 引用数据类型传递的是引用地址的副本
  • 虽然地址被复制,但它们指向同一个对象
  • 因此,修改对象属性会影响外部变量,但重新赋值不会

示例验证

function modifyReference(obj) {
    obj = { x: 100 }; // 重新赋值,改变引用地址
    console.log('函数内部的 obj:', obj);
}

let originalObj = { x: 1 };
modifyReference(originalObj);
console.log('函数外部的 originalObj:', originalObj); 

image.png

2. 不可变对象的特殊情况

基本数据类型的值是不可变的,但引用数据类型的属性是可变的。如果需要确保对象不被修改,可以使用 Object.freeze() 或深拷贝(不是很必要就别使用)。

示例代码

function tryModifyFrozen(obj) {
    obj.x = 100; // 静默失败(严格模式下报错)
}

let frozenObj = Object.freeze({ x: 1 });
tryModifyFrozen(frozenObj);
console.log('冻结后的对象:', frozenObj);

image.png

六、代码设计和注意

理解参数传递机制对编写健壮的代码至关重要:

  1. 避免意外修改:在处理引用数据类型时,如需保持原始数据不变,应进行深拷贝
  2. 函数设计:明确函数是否应该修改传入的参数
  3. 性能优化:大对象传递时,复制引用比复制整个对象更高效

七、总结

JavaScript 中的参数传递遵循以下规则:

  • 基本数据类型:传递值的副本,函数内部修改不影响外部
  • 引用数据类型:传递引用地址的副本,函数内部修改会影响外部(除非重新赋值)

总之,记住一个核心原则:JavaScript 中所有参数传递都是值传递,区别在于传递的是值本身还是引用地址