别再说 JavaScript 对象是引用传递了!从内存模型彻底理解参数传递机制

196 阅读3分钟

一、核心结论

JavaScript 中所有函数参数都是按值传递的。这个"值"指的是存储在栈内存中的值,即:

  • 对于基本类型,传递的是值的拷贝
  • 对于引用类型,传递的是引用地址值的拷贝

二、基本类型的参数传递

function changeValue(num) {
    num = 200;
    return num;
}

let x = 100;
changeValue(x);
console.log(x); // 仍然是 100

当传递基本类型时:

  1. 在栈内存中创建一个新的值拷贝
  2. 函数内的形参获得这个值的副本
  3. 对形参的任何修改都不会影响原始值

三、引用类型的参数传递

function changeObject(obj) {
    obj.name = "new name";    // 会影响原始对象
    obj = { name: "another" }; // 不会影响原始对象
}

let person = { name: "original" };
changeObject(person);
console.log(person.name); // "new name"

当传递引用类型时:

  1. 栈内存中复制的是引用地址(指针)
  2. 形参和实参指向堆内存中的同一个对象
  3. 通过引用修改对象的属性会影响原始对象
  4. 给形参赋值新对象不会影响实参的引用

svg.svg

四、关键理解点

  1. 统一的传值规则

    • 无论是什么类型的参数,栈内存中都会创建一个新的值
    • 区别仅在于基本类型传递的是数据值,引用类型传递的是引用地址值
  2. 引用类型的特殊性

    • 虽然引用地址是按值传递的,但因为指向同一个对象,所以修改对象属性会影响原始对象
    • 只有给形参赋值新对象时,才会改变形参的引用,而不影响实参
  3. 从内存角度理解

    • 栈内存:存储基本类型值和引用地址
    • 堆内存:存储引用类型的对象内容
    • 参数传递时只复制栈内存中的内容

五、常见误区

  1. "引用传递"这个说法的误解

    • JavaScript 中并不存在真正的引用传递
    • 传递的总是值,只不过对于对象来说,这个值是引用地址
  2. 混淆对象属性的修改和引用的修改

    • 修改对象属性:会影响原始对象
    • 修改引用指向:不会影响原始对象

六、实际应用建议

  1. 函数设计

    • 如果不希望修改原始对象,应在函数内创建对象的拷贝
    • 明确函数是否会修改传入的对象,并在文档中说明
  2. 代码可维护性

    • 理解参数传递机制有助于写出可预测的代码
    • 避免在函数内部直接修改对象,除非这是明确的设计意图
  3. 性能考虑

    • 对大对象进行值传递时,只传递引用地址而不是复制整个对象
    • 这种机制使得 JavaScript 在处理大对象时仍然保持高效

七、总结

JavaScript 的参数传递机制是一致的:都是按值传递。理解这一点对于写出可靠的代码至关重要。当我们说对象是按引用传递时,实际上是引用地址的值传递,这种理解可以帮助我们更好地预测和控制函数的行为。