JavaScript函数传参原理详解——值传递还是引用传递

发布时间 2023-05-29 09:29:44作者: 当下是吾

讨论JavaScript的传参原理之前,我们先来看一段曾经让笔者困惑了一段时间的代码

var testA=1;
var testB={};
function testNumber(example){
    example=2;
}
 
function testObj(example) {
    example.test=1;
}
 
testNumber(testA);
testObj(testB);
console.log(testA);//输出1
console.log(testB);//输出{test:1}

上述代码展示了一个比较纠结的问题:传入一个变量到函数中,函数对这个变量进行修改,到底会不会影响到原变量?从上面的代码我们可以发现,两者都可能出现。那到底是为什么呢?

解答这个问题之前,我们先来了解一下编程语言中函数传参的常用方式

穿插科普——实参和形参
所谓形参,是指我们定义函数的时候,函数定义的参数,例如上述代码中,testNumber函数定义中,example就是形参。

所谓实参,是指我们调用函数的时候,实际传入的值,例如上述代码中,testNumber(testA),此时testA为实参。

穿插科普——值类型和引用类型

(1)值类型:

1、占用空间固定,保存在栈中(当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;栈中存储的是基础变量以及一些对象的引用变量,基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的数组或者对象的地址,这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。)

2、保存与复制的是值本身

 

3、使用typeof检测数据的类型

 

4、基本类型数据是值类型

(2)引用类型:

1、占用空间不固定,保存在堆中(当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。)

 

2、保存与复制的是指向对象的一个指针

 

3、使用instanceof检测数据类型

 

4、使用new()方法构造出的对象是引用型

 

按值传递
按值传递是一种比较容易理解又使用比较广泛的传参方式,这种方式在传参的时候,在内存中会直接把实参的值复制一份再把副本传递给形参,对于形参的修改并不会影响到实参

按引用传递
按引用传递相对来说比较难理解,如果函数使用按引用传递,那么形参将会直接接收实参的引用,而不经过复制,那么此时对于形参的修改则会影响到实参。难以理解?看图(注意,图示仅为举例用于说明按引用传递,事实上图示的情况在js中并不会出现,具体原因下文会有说明)

 

由于在js中,引用类型在内存中分两部分存放,实际的值存放在堆中,在栈中会存放引用类型位于堆中的地址,而我们平时操作的,都是通过栈中的地址对对象进行操作的,那么如果使用按引用传递,就意味着操作的是同一个地址,对于形参的修改就会影响到实参

js中的传参策略
那么按照上面的分析,可能会有人认为,在js中,对于值类型是按值传递,对于引用类型是按引用传递,然而,这是错误的。事实上,在js中,不管对于值类型还是引用类类型,都是按值传递的,区别在于,对于值类型,传参发生时,复制的是类型本身的值,而对于引用类型,复制的是类型的地址。我们来看下面这段代码,可以用来否定引用类型是按引用传参这个观点

var testC={};
function testObject(example){
    example={b:1};
}
testObject(testC)
console.log(testC);//输出{},实参并没有改变

通过上面的代码我们可以看出,如果是按引用传参,那么直接修改形参,是会对实参造成影响的,但是我们发现事实上并没有,为了方便理解,下面给出JavaScript中值类型和引用类型进行传参时在内存中的实际复制情况

值类型

 

 

引用类型

 

对于js中的变量,值类型存放在栈中,引用类型的地址存放在栈中,对应的值存放在堆中。当传参发生的时候,值类型会直接将栈中的值进行复制,形参和实参此时实际上是两个完全不相干的变量。对于引用类型,传参发生时,会将实参变量位于栈中的地址进行复制,此时栈中会有两个指向同一个堆地址的指针。

啰嗦一下对比testObj和testObject两个函数的不同效果
我们回头看一下上面举例的两个函数,都是直接对形参进行修改,为什么一个影响到了实参,而另一个却完全没有影响呢?为了方便对比,我们把它们放到一起

对比testObj和testObject两个函数的不同效果

我们回头看一下上面举例的两个函数,都是直接对形参进行修改,为什么一个影响到了实参,而另一个却完全没有影响呢?为了方便对比,我们把它们放到一起

var testB={};
var testC={};
function testObject(example){
    example={b:1};
}
 
function testObj(example) {
    example.test=1;
}
 
 
testObject(testC);
testObj(testB);
console.log(testC);//输出{}
console.log(testB);//输出{test:1}

事实上,由于对引用对象的地址复制,形参和实参之间还是存在关联的,地址指向了同一个对象,也就是说我们调用testObj操作形参时,对应操作的对象也是实参。但是调用testObject时,就是另一种状况了,当我们直接将形参替换成另一个值,在内存中会形成下图的情况

 此时,形参会指向堆中的另一个值,形参和实参从此彻底分道扬镳,无论怎么修改,都不会互相影响了

由上例子我们可总结:

  JS的函数参数传递是按值传递, 只不过这里的值是指栈区的值。

  无论形参是值类型还是引用类型的值,在函数里都会复制出一份新的栈区值指向它们,

  如果形参是值类型的,新复制出来的栈区值是自己本身的值,此时无论如何修改形参的值都不会影响到实参。

  如果形参是引用类型,新复制出来的栈区值是指向堆中具体的某一个对象,

    1、当传入形参的对象是原先就有的,那么形参与实参都共同指向该对象,此时修改形参的值就会影响到原先对象的值。

    2、当传入形参的对象是新的对象,那么形参指向该新对象,此时无论如何修改形参的值都不会影响到实参。

 

参考链接: JavaScript函数传参原理详解——值传递还是引用传递