js是按值传递还是按引用传递

变量在内存中的存储方式

要弄清楚这个问题,首先要知道变量在内存中的存储方式,js中的数据类型分为两类:

  1. 基本类型:包括 Number、String、Boolean、Undefined、Null
  2. 引用类型:包括 Object、Array、Date、Function 等

对于这两种数据类型,有不同的内存分配,我们声明如下两个变量

1
2
var a = 100;
var b = { age: 30 };

通过下面这张图就可以看清楚变量 a 和 b 在内存中的存储方式
内存分配方式

  • 基本类型:存储在栈(stack)中的简单数据段,他们的值直接存储在变量访问的位置。
    这是因为基本类型占据的空间是固定的,可以存储在较小的内存区域栈中,以便于快速查找变量的值
  • 引用类型:存储在堆(heap)中的对象,这个时候,存储在变量处值其实是一个指针(point),指向该对象在内存中的地址。这是因为引用值是可变的(可以动态增加新的属性),所以不能存储在栈中,否则会降低变量的查找速度。

复制变量时的不同

现在我们增加两个变量 c 和 d

1
2
3
4
var a = 100;
var b = { age: 30 };
var c = a;
var d = b;

这个时候变量 c、d 在内存中的存储方式变成了这样
内存分配方式

此时我们改变 c、d 的值,看一下 a、b 是否也会被改变

1
2
3
4
5
6
c = 200;
d.sex = '男';
console.log(c); // 200
console.log(a); // 100
console.log(d); // { age: 30, sex: '男' }
console.log(b); // { age: 30, sex: '男' }

上面代码中我们是给 d 新增了一个 sex 属性,如果是重新给 d 赋值,结果又会怎么样呢?

1
2
3
d = { name: 'gwyi' };
console.log(d); // { name: 'gwyi' }
console.log(b); // { age: 30, sex: '男' }

为什么会出现这样的结果呢?给 d 重新赋值之后,并没有改变 b 的值,这是因为给 d 重新赋值之后,d 就指向了堆内存中的 { name: ‘gwyi’ },而不再指向 { age: 30 }
内存分配方式

  • 基本类型:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,这两个变量是相互独立的,更改其中一个的值不会影响到另一个
  • 引用类型:在将一个保存着对象内存地址的变量赋值给另一个变量时,会把这个内存地址赋值给新的变量(并不会在堆内存中新生成一个一模一样的对象),此时这两个变量都指向了堆内存中的同一个对象,对其中任何一个属性值的修改都会同步反映在另一个身上

什么是按值传递?按引用传递?

看下面的 c++ 中的例子来解释什么是值传递和引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//值传递
void change1(int n){
n++;
}
//引用传递
void change2(int & n){
n++;
}
int main() {
int a = 10;
change1(a);
cout<<'a='<<a<<endl; // a=10
change2(a);
cout<<'a='<<a<<endl; // a=11
}

  • 值传递:形参是实参的拷贝,改变形参的值并不会影响外部实参的值。change1使用的是值传递,当修改形参 n 的值时,实参 a 的值并没有被改变
  • 引用传递:形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。change2使用的是引用传递,当修改形参 n 的值时,实参 a 的值也会被改变

js中参数的传递方式

js中的基本类型是按值传递的

1
2
3
4
5
6
function change(n) {
n++;
}
var a = 10;
change(a);
console.log(a); // 10 不受 n++ 的影响

再来看引用类型

1
2
3
4
5
6
function change(obj) {
obj.name = 'gwyi';
}
var a = { age: 30 };
change(a);
console.log(a); // { age: 30, name: 'gwyi' } a 的值被修改了

a 的值被修改了,说明不是按值传递,那就是按引用传递吗?再来看这个例子

1
2
3
4
5
6
function change(obj) {
obj = { name: 'gwyi' };
}
var a = { age: 30 };
change(a);
console.log(a); // { age: 30 } a 的值没有被修改

如果是按引用传递,当修改形参 obj 的值时,a 的值应该也会被修改,但实际情况是 a 的值并没有被修改,说明并不是按引用传递,那js中引用类型的值究竟是按什么方式传递的呢?

按共享传递

准确的说,js中的基本类型是按值传递,引用类型是按共享传递(call by sharing),最早由 Barbara Liskov 提出,该求值策略被用于 Python、Java、Ruby、JavaScript 等多种语音

该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对形参的重新赋值,不会影响实参的值。这就是上面这个例子中 a 的值没有被修改的原因

1
2
3
4
5
6
function change(obj) {
obj = { name: 'gwyi' };
}
var a = { age: 30 };
change(a);
console.log(a); // { age: 30 } a 的值没有被修改

其实这就和上面“复制变量时的不同”(上面图三所示)写到的一样,开始的时候 a 和 obj 都指向堆内存中的 { age: 30 },当为 obj 重新赋值时,obj 就不在指向 { age: 30 } 这个对象,而是指向 { name: ‘gwyi’ } 这个新的对象

而对于下面这种 a 的值被修改了的情况,就和上面图二所示的一样, a 和 obj 都指向堆内存中的 { age: 30 },此时 obj 新增的属性值也会同步影响 a 的值

1
2
3
4
5
6
7
function change(obj) {
obj.name = 'gwyi';
console.log(obj); // { age: 30, name: 'gwyi' }
}
var a = { age: 30 };
change(a);
console.log(a); // { age: 30, name: 'gwyi' } a 的值被修改了

以上内容也是自己在看了其他同学的解答之后整理出来的,并非自己原创!

参考链接
知乎:“苏墨橘”回答javascript传递参数如果是object的话,是按值传递还是按引用传递?
JS是按值传递还是按引用传递?

坚持原创技术分享,您的支持将鼓励我继续创作!