从C语言开始
有时候讲一些细节或是底层的东西,我喜欢用C语言来讲,因为用C更方便来描述内存里面的东西。先举一个例子,swap函数,相信有一些编程经验的人都见识过,声明如下,函数体我就不写了,各位脑补一下。
- void swap1(int a, int b);
- void swap2(int* a, int* b)
这里swap1是不能交换两个数的值的,swap2可以。那为什么呢?有教材会说,第一个是值传递,第二个是引用传递,传递的是指针,所以第二个可以。好吧,这个解释和没说一样,那下面我就来解释一下,调用这两个函数的时候,到底发生了什么,为什么一个可以交换,另一个不可以。为了方便描述,我把这两个函数的调用代码也写出来
- int main() {
- int a = 3;
- int b = 4;
- swap1(a, b); //此时a = 3, b = 4;
- int* pa = &a;
- int* pb = &b; //为了方便解释,增加这两个临时变量,否则直接写swap2(&a, &b)的话,这行代码做的事情太多,不好解释。
- swap2(pa, pb); //此时a = 4, b = 3;
- return 0;
- }
函数的执行是在栈中,下图描述了swap1执行开始和结束的时候,栈中的情况。
左图为执行前,右图为执行后。当main函数调用swap1函数的时候,将两个入参a,b压栈,压栈采用的是复制的方式,当swap1执行的时候,修改了swap1栈空间的两个值,但是main函数中的两个值没有受影响。这就是值传递。把入参的值复制压栈来传入参数。下面来看swap2的情况
左图为执行前,右图为执行后。其中最左一列为内存地址。这里地址,压栈方向,地址顺序均为示例。各位看到没有,所谓引用传递,其实还是值传递,传递的时候还是采用复制压栈,只是传递的“值”是个地址。swap2执行的时候,执行*a = temp.给*a 赋值,这行语句的意思是修改a这个地址指向的内存的值,由于这个地址指向的位置在main方法的栈空间中,所以实现了修改原来值。
以上就是C语言在实现swap函数时候内存的细节,下面我们来探讨一下JS中调用函数的情况。由于JS中没有&这个取地址符号,那么JS中传递的到底是什么呢?
回到JS
JS中的数据类型有数字,布尔,数组,字符串,对象,null, undefined. 那么当用这些数据类型来作为函数的参数的时候,到底是引用传递还是值传递呢?先说结论:布尔,数字是值传递,字符串,数组,对象是引用传递。事实上字符串,数组也可以作为对象。null和undefined在传递的时候到底是什么,我不清楚。如果有熟悉的大神请帮忙解释一下。在这里小弟先谢了。
布尔,数字在作为参数传递的时候,其实现和C语言一样,这里不做赘述。也是将调用者的局部变量复制压栈,传递给被调用者。下面我来详细的描述一下对象是如何传递的。以一个函数来举例,假设你需要实现一个函数,将一个传入的数组反序,reverse,下面有两个实现,请各位来看一下有什么问题:
- function reverse1(array) {
- var temp = [];
- for (var i = array.length - 1; i > -1; i--) {
- temp.push(array[i]);
- }
- array = temp;
- }
- function reverse2(array) {
- var temp = [];
- for (var i = array.length - 1; i > -1; i--) {
- temp.push(array[i]);
- }
- for (var i = 0; i < array.length; i++) {
- array[i] = temp[i]
- }
- }
这两个函数都是先将一个反序完成的数组存储在temp里面,然后赋值给入参array,就是赋值的方式有所不同。这个不同的赋值方式也导致了结果的不同,结果就是reverse1无法完成工作,reverse2可以。为了解答这个问题,我先讲一下JS里面,内存中对象是如何存储的。当一行代码 var temp = [] 被运行的时候,内存中是这样的:
其中蓝色的是栈,黑框的是堆,用来动态分配内存,最右绿色的表示这段堆的起始地址。也就是说当声明一个对象的时候,栈中保存的内容只是一个指针,真正的内容在堆中。以此为基础,我们再来看一下当函数reverse1执行的时候,内存中如何实现的。为方便举例,假设传入的数组为[1,2,3];
上图为执行前,下图为执行后。当函数reverse1执行时,in作为参数传入。传入参数时,类似C语言的引用传递,将地址复制了一份,压栈传到子函数中。所以两个函数中的变量是指向同一个位置的。当reverse1执行时,temp中存储了array的反序,最后一行赋值的时候,你就看到了如下面的图表示的那样,reverse1中的array确实指向了新的反序数组,但是调用者中的局部变量in却丝毫未动。所以导致了reverse1无法完成反序功能。
那么我们再看reverse2. reverse2中的第二个循环逐个给数组的内容复制,其实它操纵的内存空间就是array指向的区域,我们又知道array和in指向了同一个区域,所以in指向的区域也被改变了。
总结一下以上所说的,
- JS中布尔,数字为基本数据类型,是值传递。无法作为引用传递。所以JS中无法实现基本数据类型的swap函数。
- 对象是引用传递。当传递对象给子函数时,传递的是地址。子函数使用这个地址来操作修改传入的对象。但是如果在子函数修改该地址指向的位置时,这个改变将无法作用于调用者。
- 引用传递其实还是值传递,只是传入的值是个地址,并且该地址指向了一段保存了对象数据的内存。这点和C中的引用传递类似。
特别说一下String
String是JS的内置对象,所以根据上文所说,它是引用传递。那么下面我请你写一个函数,将传入的String修改,给它两头加上引号。所以很明显,下面这样的函数就是错误的了
- function foo(s) {
- s = """ + s + """
- }
- var a = 1
- var b = 1
- a == b //true
- a === b //true
- var s1 = "sdf"
- var s2 = "sdf"
- s1 == s2 //true
- s1 === s2 //true
- s3 = new String("sdf")
- s1 === s3 //false
对于数字,估计各位没有疑问吧。那么对于字符串来说,== 比较的是两个字符串的内容,这个应该也没有疑问。那么===呢?并且为什么s1===s2为true,s1===s3为false呢?
当用===来比较字符串的时候,事实上比较的是两个对象的地址。s1的值“sdf”这个字符串的地址,s3则是一个新的对象的地址。他们不相等,这个很好理解。那么s1 和 s2如何解释呢?这因为JS引擎有一个静态字符串存储区,当声明一个字符串常量的时候,会先去该存储区查找有没有相同的字符串,如果有就返回该字符串,没有再在静态字符串区重新初始化一个字符串对象。这就解释了为什么s1 === s2.
顺便说一句,就是字符串的不可变性,以及常量字符串区这两个特性,Java和JS是一样的。然而C++的STL中的std::string是可变的。
注:本文中的JS执行时的内存示例图并不是真正的JS引擎执行时候物理内存的样子。物理内存的实现取决于JS引擎。