我们都知道在 ECMAScript 中,数据类型分为原始类型(又称值类型/基本类型)和引用类型(又称对象类型);这里我将按照这两种类型分别对函数进行传参,看一下到底发生了什么。
参数的理解
首先,我们要对函数的参数有一个了解:
形参就是函数内部定义的局部变量;
实参向形参传递值的时候,就是一个赋值操作,把实参的值直接复制一份给形参。
原始类型参数传递
示例1
var a = 1;
function f(b) {
a = 3;
}
f(a);
console.info(a); // 3
示例1中的代码比较简单,解析如下:
- 首先,我们定义了一个变量
a
,给它赋值为1
;又定义了一个函数f
,函数f
的形参是b
(此时,相当于在函数f
里定义了一个变量var b
; 它的值现在是undefined
); - 调用函数
f(a)
把a
作为实参传入。这里可以理解为,给b
进行了一次赋值操作b = 1
; - 接下来继续执行代码
a = 3
;注意:在函数体里边,出现了一个变量a
,但是它没有用var
关键字定义,所以,它是一个全局作用域的变量,不是这个函数的局部变量,而在一开始,我们就定义了这样一个变量a
,所以在这里就是对之前的a
进行的又一次赋值操作,把a
从之前的1
变成了3
; - 执行完
f(a)
之后,把全局变量a
的值改变了,所以,当我们输出a
查看它的值时,就得到了3
,而对于函数的形参b
根本没有进行任何操作而已。
示例2
var a = 1;
function f(a) {
a = 3;
}
f(a);
console.info(a); // 1
解析:
示例2与示例1的区别,从表面上看,就是形参 b
变成了 a
。但是,这样的变化结果就是,对于函数 f
来说,参数起到了作用,当我们对函数 f
进行传参操作的时候,我们传入的实参在函数内部就会得到引用。
相比较示例1的第一步,函数 f
内部定义了一个变量 a
它的值是 undefined
,注意:在未执行函数的时候,只是进行了预解析,代码没有执行,在调用函数的时候才会开始执行代码。
执行 f(a)
后,就是传入实参 1
,函数内部的变量 a
赋值为 1
,然后再进行 a = 3
的操作 ,此时,在函数的局部作用域的栈内存中有一个变量 a
它的值是 3
;而在全局作用域的栈内存中,也存在一个变量 a
,它的值是 1
,这两个变量是两个不同的变量,只是它们的名字都是 a
而已。
过程如图:
示例3
var a = 1;
function f(b) {
a = 3;
b = 10;
}
f(a);
console.info(a); // 3
console.info(b); // 报错
解析:
下面我们再来看一下示例3,先看变量 a
,完全和示例1一样,这里就不再详细说明了;再看变量 b
,其实也是和示例2没任何不一样的地方的,只是在这里把形参的名字改变了一下而已,在最后我们输出 a
的时候,没有任何问题,结果是 3
,但是,访问变量 b
的时候,输出会报错,这里涉及到的就是作用域问题了:在函数内部定义的变量,在函数内部可以访问,而在函数的外部是无法访问的。
具体过程可看下图:
示例4
var a = 1;
function f(a) {
a = 3;
b = 10;
}
f(a);
console.info(a); // 1
console.info(b); // 10
解析:
综合前三个示例,示例4我想大家都应该没有什么问题了,我们直接来看图吧:
引用类型参数传递
下面我们再来两个引用类型参数的示例;以下示例仅为说明引用类型传参之后,函数内部的赋值变化,所以用的都是简单数组进行说明。
其实,引用类型参数的传递需要考虑的就是引用类型和原始类型之间的区别:
- 引用类型在预解析时候,和原始类型一样都会在栈区里分配到空间,生成变量;但是赋值的时候,原始类型的赋值依然保存在栈内存当中,而引用类型就会在堆内存里占用一定空间存放它的数据,而在栈区里生成一个指向堆区的地址。
- 对变量进行复制的时候,原始类型的复制是在栈内存当中生成一份一样的数据,可以理解为“完全复制”;而引用类型的复制,只是在栈内存中进行复制,两个变量的地址同时指向堆内存中的那一份数据。
示例5
var a = [1];
function f(a){
a[100] = 3;
}
f(a);
console.info(a); // [1,100:3]
解析:
第一步:全局栈区中生成变量 a
,赋值后,在全局堆区里生成数组 [1]
;
第二步:传参,调用 f(a)
,首先在函数栈区中生成变量 a
,此时值为 undefined
,然后把全局变量 a
的值赋给局部函数里的变量 a
,因为是引用类型的数组,所以,函数里的 a
在栈区里也生成一个指向全局堆里的相同 地址1
;
第三步:执行函数里的代码 a[100] = 3
,找到函数栈中的 a
,再对它进行赋值,改变了全局堆中的 [1]
,得到了一个新的数组 [1,100:3]
;
第四步:访问全局作用域中的变量 a
,得到指向堆内存中的数组 [1,100:3]
。
示例6
var a = [1];
function f(b){
b[100] = 3;
b = [1,2,3];
console.info(b); // [1,2,3]
}
f(a);
console.info(a); // [1,100:3]
解析:
该示例的关键点在第四步,执行到代码 b = [1,2,3]
的时候,会对函数里面的变量 b
重新进行一次赋值,这样会在函数的堆内存中生成一个新的 数组,全局的 a
和函数中的 b
的指向地址就不一样了,所以,它俩的输出就不一样了。
总结
在 js 中,原始类型是按值传递的;引用类型是按共享传递的。
按值传递 - call by value
一个外部变量传递给一个函数时,函数实参获取到的实际上是这个外部变量的副本,在函数内部我们对实参进行的修改并不会反应到这个外部变量上。
按引用传递 - call by reference
一个外部变量传递给一个函数时,函数实参实际上是这个外部变量的一个引用,我们在函数内部对这个实参的任何修改,都会反应到这个外部变量。