• JavaScript进阶(三) 值传递和引用传递


    从C语言开始

    有时候讲一些细节或是底层的东西,我喜欢用C语言来讲,因为用C更方便来描述内存里面的东西。先举一个例子,swap函数,相信有一些编程经验的人都见识过,声明如下,函数体我就不写了,各位脑补一下。
    [cpp] view plaincopy
     
    1. void swap1(int a, int b);  
    2. void swap2(int* a, int* b)  

    这里swap1是不能交换两个数的值的,swap2可以。那为什么呢?有教材会说,第一个是值传递,第二个是引用传递,传递的是指针,所以第二个可以。好吧,这个解释和没说一样,那下面我就来解释一下,调用这两个函数的时候,到底发生了什么,为什么一个可以交换,另一个不可以。为了方便描述,我把这两个函数的调用代码也写出来
    [cpp] view plaincopy
     
    1. int main() {  
    2.     int a = 3;  
    3.     int b = 4;  
    4.     swap1(a, b);    //此时a = 3, b = 4;  
    5.     int* pa = &a;  
    6.     int* pb = &b;     //为了方便解释,增加这两个临时变量,否则直接写swap2(&a, &b)的话,这行代码做的事情太多,不好解释。  
    7.     swap2(pa, pb);    //此时a = 4, b = 3;  
    8.     return 0;  
    9. }  

    函数的执行是在栈中,下图描述了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,下面有两个实现,请各位来看一下有什么问题:
    [javascript] view plaincopy
     
    1. function reverse1(array) {  
    2.     var temp = [];  
    3.     for (var i = array.length - 1; i > -1; i--) {  
    4.         temp.push(array[i]);  
    5.     }  
    6.     array = temp;  
    7. }  
    8.   
    9. function reverse2(array) {  
    10.     var temp = [];  
    11.     for (var i = array.length - 1; i > -1; i--) {  
    12.         temp.push(array[i]);  
    13.     }  
    14.     for (var i = 0; i < array.length; i++) {  
    15.         array[i] = temp[i]  
    16.     }  
    17. }  

    这两个函数都是先将一个反序完成的数组存储在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指向的区域也被改变了。

    总结一下以上所说的,

    1. JS中布尔,数字为基本数据类型,是值传递。无法作为引用传递。所以JS中无法实现基本数据类型的swap函数。
    2. 对象是引用传递。当传递对象给子函数时,传递的是地址。子函数使用这个地址来操作修改传入的对象。但是如果在子函数修改该地址指向的位置时,这个改变将无法作用于调用者。
    3. 引用传递其实还是值传递,只是传入的值是个地址,并且该地址指向了一段保存了对象数据的内存。这点和C中的引用传递类似。

    特别说一下String

    String是JS的内置对象,所以根据上文所说,它是引用传递。那么下面我请你写一个函数,将传入的String修改,给它两头加上引号。所以很明显,下面这样的函数就是错误的了
    [javascript] view plaincopy
     
    1. function foo(s) {  
    2.     s = """ + s + """  
    3. }  
    那么正确的函数应该怎写呢?你可能会想,应该使用String对象的函数来修改String的内容。这么想是对的,但是很不幸,JS提供的String没有任何一个可以修改String内容的函数。有人说不对,比如字符串连接函数,concat,转大小写函数toUpperCase,toLowerCase。事实上这两个函数只是返回了一个新的String对象,其原本的值兵没有改动。这个你可以去做实验看看。所以String对象被建立好之后,就再也无法改动了,所以无法用一个子函数来修改它的值。又由于String可以用 == 来判断其内容是否相等,所以它的各方面特性都很像基本数据类型。但是还有一点不一样,请看下面的例子:
    [javascript] view plaincopy
     
    1. var a = 1  
    2. var b = 1  
    3. a == b            //true  
    4. a === b           //true  
    5.   
    6. var s1 = "sdf"  
    7. var s2 = "sdf"  
    8. s1 == s2          //true  
    9. s1 === s2         //true  
    10. s3 = new String("sdf")  
    11. 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引擎。
  • 相关阅读:
    Nginx 反向代理多个后台服务端口
    微信小程序,横向布局,纵向布局
    Maven的标准settings.xml文件
    Springboot 复杂查询及SQL拼接笔记
    ElementUI 设置显示侧栏滚动条elscrollbar,隐藏横向滚动条
    让俺内牛满面的编辑器啊~
    Javascript 对话框 (遇到 Ajax Load无法加载问题)
    thinkpad s5 电源功率不足提示
    NAO机器人开发环境配置
    Choregraphe 2.8.6.23动作失效
  • 原文地址:https://www.cnblogs.com/smght/p/4368395.html
Copyright © 2020-2023  润新知