javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS存不存在引用传递

JavaScript中到底存不存在“引用传递”详解

作者:代码里的小猫咪

在Javascript中我们使用函数并且传递参数来调用函数,但是Javascript到底是如果传递你所传递的参数呢?这篇文章主要介绍了JavaScript中到底存不存在引用传递的相关资料,需要的朋友可以参考下

一、概述

在编程语言中,“值传递”(pass by value)和“引用传递”(pass by reference)是两种常见的参数传递方式:

不同语言可能支持其中一种或两种。比如 C 语义默认值传递,若要“引用传递”需要传递指针;C++ 支持两者;Python 一切都是对象引用的“值传递”(亦即“传递引用的副本”);Java 中基本类型值传递,对象也是“传递引用的副本”;而 JavaScript 的传参机制常被误解为也有引用传递,严格意义上来说 JavaScript 只有“值传递”,但对于对象类型,所传递的“值”是一个“指向对象的引用”,这导致看似像“引用传递”的效果。

二、JavaScript 的内存模型

首先了解一下 JS 的内存管理与内存布局。

  1. 栈(Stack)
    存放局部变量、函数调用时的上下文(execution context)、基本类型的值等。栈区空间小且分配回收快。

  2. 堆(Heap)
    存放对象、数组、函数等“引用类型”数据。堆区空间大,但分配回收相对慢。

比如:

let a = 10;
let obj = { x: 1, y: 2 };

三、值传递与引用传递的定义

3.1 值传递(Pass by Value)

function foo(x) {
  x = 100;
}
let a = 1;
foo(a);
console.log(a); // 1 —— a 未被修改

a 和 x 各自存储在不同的位置,函数内部对 x 的修改,不会反映到 a。

3.2 引用传递(Pass by Reference)

示例:支持引用传递的 C++

void foo(int &x) {
  x = 100;
}
int a = 1;
foo(a);
cout << a; // 100 —— a 被修改

C++ 中用 & 声明引用参数,就能直接修改实参。 

四、JavaScript 只有“值传递”

4.1 基本类型 —— 绝对的值传递

JS 的基本类型包括:undefined、null、boolean、number、string、symbol、bigint。这些类型的变量在赋值、传参时,都是将值本身复制一份。

function changeNum(n) {
  n = 99;
}
let num = 10;
changeNum(num);
console.log(num); // 10 —— 仍然是 10

无论在函数内如何修改 n,都无法影响外部的 num。

4.2 引用类型 —— 传递的是“引用的值”

JS 的引用类型包括:Object、Array、Function、Date、RegExp 等。它们存在堆上,栈上保存的是一个“引用”(内存地址的值。

当将引用类型作为参数传递时,复制的是这个 引用本身的值并非直接复制对象。因此:

function changeObj(o) {
  o.x = 100;      // 修改同一对象的属性
  o = { x: 200 }; // 重新给 o 赋了一个新对象
}
let obj = { x: 1 };
changeObj(obj);
console.log(obj.x); // 100 —— 改变属性生效,但重新赋值不影响外部引用

简单分析一下:

1、创建 obj

let obj = { x: 1 };

内存结构如下:

变量名   |  值
--------|-----------------
obj     |  指向 --> { x: 1 }(对象)

2、调用 changeObj(obj)

changeObj(obj);

现在 obj 的值(即引用地址)被复制一份传给函数形参 o。可以理解为:

o = obj(引用地址被复制了)

此时内存情况如下:

变量名   |  值
--------|-----------------
obj     |  --> { x: 1 }
o       |  --> { x: 1 }  (和 obj 指向同一个对象)

3、o.x = 100

修改了对象的属性:{ x: 100 },形参 o 和外部 obj 引用同一个对象,故属性修改影响可见。

现在内存为:

变量名   |  值
--------|-----------------
obj     |  --> { x: 100 }
o       |  --> { x: 100 }  (仍然是同一个对象)

4、o = {x: 200}

这一行的关键点是:

只是给 o 这个局部变量重新赋值,指向了一个新对象,它和原来的 obj 再也没有关系!此时外部 obj 仍指向旧对象,不受影响。

现在内存状态是:

变量名   |  值
--------|-----------------
obj     |  --> { x: 100 }
o       |  --> { x: 200 }(一个新的对象)

也就是说,只是“切断”了 o 和 obj 的连接,但 obj 依旧指向旧的对象。 

想要影响外部变量 obj 本身的引用(让它指向新对象),JS 是做不到的(因为无法通过函数改变调用者作用域中的变量绑定)。

延伸思考:那如果想改变外部引用呢

那就需要 返回一个新对象

function changeObj(o) {
  return { x: 200 };
}

let obj = { x: 1 };
obj = changeObj(obj); // 手动接受返回值
console.log(obj.x);   // 200 

总结:

JS 中没有实参“地址传递给形参”,而是“地址的值”被复制给形参。这里的“引用传递”说法,只是因为复制的是对象的引用而已,函数内部也只是在该引用上做操作。如果改写引用本身,不会反向影响。

五、细分场景

5.1 基本类型

function foo(s) {
  s += ' world';
  console.log('内部 s:', s);
}
let str = 'hello';
foo(str);               // 内部 s: hello world
console.log(str);       // 外部 str: hello

5.2 对象类型修改属性

function mutate(o) {
  o.age = o.age + 1;
}
let person = { name: 'Alice', age: 20 };
mutate(person);
console.log(person.age); // 21 —— 属性修改被保留

5.3 对象整体重新赋值

function replace(o) {
  o = { name: 'Bob', age: 30 };
}
let person = { name: 'Alice', age: 20 };
replace(person);
console.log(person.name); // Alice —— 外部对象不变

5.4 数组示例

function append(arr) {
  arr.push(4);
  arr = [1, 2];
  arr.push(3);
  console.log('内部 arr:', arr);
}
let a = [1, 2, 3];
append(a);             // 内部 arr: [1,2,3]
console.log(a);        // 外部 a: [1,2,3,4]

六、值传递 vs 引用传递的对比

特征值传递“引用传递”(误解)
传递内容基本类型值,或引用类型的引用值直接传递对象内存地址(C++ 引用语义)
修改效果不影响外部修改属性会影响外部,但重赋值不影响
内存操作复制独立值复制指针/引用
是否支持JS 全部传参模式JS 只支持前者,但复制的是引用的值,易混淆

关键:JS 中的每一次函数调用都只做“值拷贝”,不管是基本类型还是引用类型,形参拿到的都是一份拷贝。但如果拷贝的是“引用指针”,则通过该指针操作到同一块堆内存,就会有“修改可见”的效果。

6.1常见误区

  1. 误区:JS 支持引用传递
    真相:JS 传参始终是值传递,只是“值”可能是指向对象的引用。

  2. 误区:对象传参修改属性等价于引用传递
    虽然看起来像引用传递,但形参本质仍是指针的拷贝,若重写指针则不会影响外部。

  3. 误区:函数内部 arguments 改变会影响外部形参
    ES5 严格模式下已分离,非严格模式下 arguments[i] 与形参同名会关联,但这属于语言特殊行为,与传参语义不同。

6.2 深拷贝与浅拷贝

既然传递的是引用,那么如何避免“函数体内无意修改对象属性”带来副作用?通常会先对对象或数组做浅拷贝或深拷贝,再传入函数。

1、浅拷贝

对象浅拷贝:只拷贝一层属性,若属性值仍为引用类型,则拷贝的是内部引用。

let obj = { a: 1, b: { c: 2 } };
let copy = { ...obj };   // 或 Object.assign({}, obj)
copy.a = 9;
copy.b.c = 99;
console.log(obj.b.c);    // 99 —— 内部对象仍被修改

数组浅拷贝:arr.slice()、[...arr]。

2、深拷贝

let obj = { a: 1, b: { c: 2 } };
let deep = JSON.parse(JSON.stringify(obj));
deep.b.c = 100;
console.log(obj.b.c);    // 2 —— 原对象保持不变

6.3 函数式编程与不可变思想

在需要高可靠性的项目中,常常提倡“不可变数据”和“纯函数”:

function addItem(arr, item) {
  // 不修改传入的 arr,而是返回新数组
  return [...arr, item];
}

如此,即可彻底避免因“引用”导致的意外修改。

七、“引用传递”的效果

在 ES 模块(import/export)或 CommonJS (require/module.exports) 里,看似传递的是「值」,但对于引用类型,获取到的是同一个对象的引用并且模块只会初始化一次、结果会被缓存并在多个模块间共享。这就产生了“引用传递”的效果——在任意一个地方修改了这个对象,别的地方都能感知到。

7.1 为什么会这样?

  1. 模块只会执行并初始化一次
    当第一次 import 或 require 一个模块时,模块文件里的代码会被执行,导出的对象/变量就生成并存放在模块缓存里。后续所有对这个模块的导入,都是从缓存里拿到同一个“实例”。

  2. 导出的是对同一份对象的引用

    • 对象、数组、函数 等引用类型,导出时并不会把它“复制”一份给每个导入者,而是把同一个对象的引用挂到每个导入模块里。

    • 即便 ES 模块对 变量 本身采用“活绑定”(live binding),也不会把对象内容克隆一次。

  3. 共享缓存带来“引用”效果

    // a.js
    export const settings = { mode: 'light' };
    
    // b.js
    import { settings } from './a.js';
    settings.mode = 'dark';  // 改变了 a.js 中同一个对象
    
    // c.js
    import { settings } from './a.js';
    console.log(settings.mode);  // 'dark'
    

    无论在 b.js 还是 c.js 中修改 settings,因为它们都是指向同一个对象,所以彼此可见。

CommonJS(Node.js)的类似行为

对于 require,也是同样道理:

// config.js
module.exports = { url: 'https://api.example.com' };

// service.js
const cfg = require('./config');
cfg.url = 'https://api.dev';  // 改变了缓存里的对象

// index.js
const cfg1 = require('./config');
console.log(cfg1.url);  // 'https://api.dev'

require 会缓存 module.exports,后续 require 拿到的永远是同一个对象。

小结:

所以,我们所看到的“引用传递”,实际上是模块系统的 “单例缓存 + 共享引用” 机制在起作用。

八. 总结

场景是否“引用传递”实际发生了什么
函数参数传递不是引用传递始终是值传递,但引用类型的值是指针副本,因此函数内部能改对象内部属性,不能替换整个引用。
模块 import/export表现像引用传递模块导出的是同一个对象的引用,多处导入会共享这份引用,修改会同步反映。
对象赋值 let b = a表现像引用传递赋值的是引用的副本,修改对象内部属性会影响原对象。
原始类型(string, number, boolean)传参或赋值值传递完全复制一份值,互不影响。

JS 永远是值传递,但引用类型的“值”本质是一个“指向对象的地址”。

所以可以改对象内容,却无法改变原引用的绑定。

模块导出时共享引用,看起来像引用传递,但那是模块缓存机制的结果。 

到此这篇关于JavaScript中到底存不存在引用传递的文章就介绍到这了,更多相关JS存不存在引用传递内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文