JavaScript函数传参原理详解——值传递还是引用传递
讨论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、当传入形参的对象是新的对象,那么形参指向该新对象,此时无论如何修改形参的值都不会影响到实参。
p.s
我们解析函数的形参是对象时,最好使用const加解构的方式去解析或者使用JSON重新创建出一个新对象,从而避免莫名奇妙修改了值。
eg.
function demo (obj){ const {a,b,...datas}= obj || {} }
or
function demo (obj){
const copy = JSON.parse(JSON.stringify(obj))
}