网上关于JavaScript如何给函数传递值有很多误解和争论。大致认为,当参数是原始数据类时,参数是按值传递的,对于数组、对象、函数等数据类型,参数是按引用传递。值传递和引用传递参数的主要区别可以简单的说:值传递:函数内改变传递的值不会影响外部引用传递:函数内改变传递的值会影响外部,但是答案是JavaScript有所有的数据类型都是按值传递的。它对数组和对象使用按值传递,但这是在共享或复制引用中使用的按值传递。这有点抽象,所以让我们从几个例子开始,然后我们将在函数执行期间查看JavaScript的内存模型以了解实际发生的情况。按值传递参数在JavaScript中,原始类型的数据是按值传递的;对象类型和Java一样,复制一个原始对象的引用,并在这个引用上进行操作。但是在JS中,string是一种原始数据类型,而不是对象类。letsetNewInt=function(i){ii=i+33;};letsetNewString=function(str){str+="cool!";};letsetNewArray=function(arr1){varb=[1,2];arr1=b;};letsetNewArrayElement=function(arr2){arr2[0]=105;};leti=-33;letstr="Iam";letarr1=[-4,-3];letarr2=[-19,84];控制台。log('iis:'+i+',stris:'+str+',arr1is:'+arr1+',arr2is:'+arr2);setNewInt(i);setNewString(str);setNewArray(arr1);setNewArrayElement(arr2);console.log('Now,iis:'+i+',stris:'+str+',arr1is:'+arr1+',arr2is:'+arr2);运行结果iis:-33,stris:Iam,arr1is:-4,-3,arr2is:-19,84Now,iis:-33,stris:Iam,arr1is:-4,-3,arr2is:105,84这里需要注意两点:1)***字符串str通过setNewString方法传入。如果你学过C#、Java等面向对象语言,你会认为调用这个方法后str的值会发生变化。这是面向对象语言中字符串类型的对象。参数通过引用传递,因此在此方法中更改str也会更改外部。但是在JavaScript中,前面说过,在JS中,string是数据的原始类型而不是对象类,所以是按值传递的,所以在setNewString中改变str的值不会影响到外部。2)第二种是通过setNewArray方法传递数组arr1,因为数组是对象类型,所以是引用传递。在这个方法中,我们改变了arr1的指针,所以如果是面向对象的语言,我们认为***结果arr1的值就是被重定向的那个,也就是[1,2],但是***打印结果显示arr1的值还是原来的值。为什么是这样?StackOverflow上上述CommunityWiki的共享传递答案是:对于传递给函数参数的对象类型,如果直接改变复制引用的指向地址,不会影响到原来的对象;如果通过复制的引用进行内部值操作,则将更改为原始对象。可以参考博文JavaScript基础(二)——JS是按值调用还是按引用调用?functionchangeStuff(state1,state2){state1.item='changed';state2={item:"changed"};}varobj1={item:"unchanged"};varobj2={item:"unchanged"};changeStuff(obj1,obj2);console.log(obj1.item);//obj1.item会被改变console.log(obj2.item);//obj2.item不会改变原因:上面的state1相当于obj1,那么obj1.item='changed',对象obj1内部的item属性改变了,自然会影响到原来的对象obj1。同样,state2就是obj2。方法中state2指向了一个新的对象,也就是改变了原来的引用地址。这不会影响外部对象(obj2)。这种现象更专业的叫作:call-by-sharing,为了方便,暂且称之为shareddelivery。内存模型JavaScript在执行期间为程序分配三部分内存:代码区、调用栈和堆。这些一起被称为程序的地址空间。代码区:这是存放要执行的JS代码的区域。调用堆:该区域跟踪当前正在执行的函数,执行计算并存储局部变量。变量以后进先出的方式存储在栈中。后进先出,这里存放的是数值数据类型。例如:varcorn=95letlion=100这里变量corn和lion的值在执行过程中被存入栈中。堆:是分配JavaScript引用数据类型(如对象)的地方。与堆栈不同,内存分配是随机放置的,没有LIFO策略。为了防止堆内存泄漏,JS引擎有内存管理器来防止它们发生。classAnimal{}//storenewAnimal()instanceatmemoryaddress0x001232//tiger的堆栈值为0x001232consttiger=newAnimal()//storenewObjectinstanceatmemoryaddress0x000001//`lion`stackvalueis0x000001letlion={strength:"VeryStrong》}这里,lion和tiger是引用类型,其值存储在堆上并压入堆栈。它们在栈上的值是堆中位置的内存地址。激活记录,参数传递既然我们已经了解了JS程序的内存模型,那么让我们看看当我们在JavaScript中调用一个函数时会发生什么。//例1functionsum(num1,num2){varresult=num1+num2returnresult}vara=90varb=100sum(a,b)在JS中每当调用一个函数时,执行该函数所需的所有信息都放在栈上。这些信息就是所谓的激活记录(ActivationRecord)。这个ActivationRecord,我直译为ActivationRecord。查了很多资料,没看到更好的中文翻译。知道的请留言。激活记录上的信息包括以下内容:SP堆栈指针:调用方法之前堆栈指针的当前位置。RA返回地址:这是函数执行完成后继续执行的地址。RV返回值:这是可选的,函数可能会或可能不会返回值。参数:将函数需要的参数压入栈中。局部变量:函数使用的变量被压入堆栈。我们必须知道这一点,我们在js文件中编写的代码在执行之前会被JS引擎(如V8、Rhino、SpiderMonkey等)编译成机器语言。所以下面的代码:letshark="SeaAnimal"会被编译成下面的机器码:0100010010101001010101010101上面的代码相当于我们的js代码。介于机器码和JS之间的是一种语言,它就是汇编语言。JS引擎中的代码生成器首先将js代码编译成汇编代码,最后生成机器码。为了了解实际发生了什么,以及在函数调用期间激活记录如何被压入堆栈,我们必须了解程序在汇编中的表示方式。为了跟踪函数调用过程中参数在JS中是如何传递的,我们将例1的代码用汇编语言表达并跟踪其执行流程。先介绍几个概念:ESP:(ExtendedStackPointer)即扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。与之对应的是EBP(ExtendedBasePointer),扩展基址指针寄存器,又称帧指针寄存器,用于存放函数栈的底指针。EBP:扩展基址指针寄存器(extendedbasepointer)里面存放了一个指针,它指向系统栈最顶层栈帧的底部。EBP只是在某个时刻访问ESP。这时,进入一个函数后,cpu会将ESP的值赋值给EBP。这时候可以通过EBP对栈进行操作,比如获取函数参数,局部变量等。其实,用ESP也是可以的。//例1函数sum(num1,num2){varresult=num1+num2returnresult}vara=90varb=100vars=sum(a,b)我们看到sum函数有两个参数num1和num2。函数被调用,分别传递90和100的a和b值。请记住:值数据类型包含值,而引用数据类型包含内存地址。在调用sum函数之前,它将其参数压入堆栈ESP->[…]ESP->[100][90][….]然后,它将返回地址压入堆栈。返回地址保存在EIP寄存器中:ESP->[OldEIP][100][90][.......]接下来,它保存基指针ESP->[OldEBP][OldEIP][100][90][....]然后更改EBP并将调用保存寄存器压入堆栈。ESP->[OldESI][OldEBX][OldEDI]EBP->[OldEBP][OldEIP][100][90][.......]为局部变量分配空间:ESP->[][OldESI][OldEBX][OldEDI]EBP->[OldEBP][OldEIP][100][90][......]这里进行加法:movebp+4,eax;100addebp+8,eax;eaxeax=eax+(ebp+8)moveax,ebp+16ESP->[190][OldESI][OldEBX][OldEDI]EBP->[OldEBP][OldEIP][100][90][....]我们返回的值为190,它被分配给EAX。movebp+16,eaxEAX是“累加器”,它是许多加法和乘法指令的默认寄存器。然后,恢复所有寄存器值。[190]DELETED[OldESI]DELETED[OldEBX]DELETED[OldEDI]DELETED[OldEBP]DELETED[OldEIP]DELETEDESP->[100][90]EBP->[.......]并将控制返回给函数被调用,压入堆栈的参数被清除。[190]DELETED[OldESI]DELETED[OldEBX]DELETED[OldEDI]DELETED[OldEBP]DELETED[OldEIP]DELETED[100]DELETED[90]DELETED[ESP,EBP]->[……]调用函数现在取回返回值从EAX寄存器到s的内存位置。moveax,0x000002;//s变量在内存中的位置我们已经看到了内存中发生的事情以及如何在汇编代码中将参数传递给函数。在调用函数之前,调用者将参数压入堆栈。所以说在js中传递参数是传递值的拷贝是正确的。如果被调用的函数改变了参数的值,它不会影响原始值,因为它存储在别处,它只处理一个副本。functionsum(num1){num1=30}letn=90sum(n)//`n`仍然是90让我们看看传递引用数据类型时会发生什么。functionssum(num1){num1={number:30}}letn={number:90}sum(n)//`n`仍然是{number:90},用汇编代码表示:n->0x002233Heap:Stack:002254012222。..0122230x002233002240012224002239012225002238002237002236002235002234002233{number:90}002232002231{number:30}Code:...000233main://entrypoint000234pushn//n值为002233,它指向堆中存放{number:90}地址。nispushed入栈0x12223。000235;//保存所有寄存器...000239callsum;//跳转到内存中的`sum`函数000240...000270sum:000271;//创建对象{number:30}内部地址main0x002231000271mov0x002231,(ebp+4);//将内存地址0x002231中的{number:30}入栈(ebp+4)。(ebp+4)是地址0x12223,即n所在的地址,也是对象{number:90}在堆中的位置。此处,堆栈位置被值0x002231覆盖。现在,num1指向另一个内存地址。000272;//清理堆栈...000275ret;//回到调用者所在的地方(000240)这里我们看到变量n保存着指向它在堆中的值的内存地址。当sum函数执行时,参数被压入堆栈以供sum函数接收。sum函数创建另一个对象{number:30}存储在另一个内存地址002231并将其放入堆栈的参数位置。将之前栈中参数位置的对象{number:90}的内存地址替换为新建对象{number:30}的内存地址。这使n保持不变。因此,复制引用策略是正确的。变量n被压入堆栈,在执行sum时使其成为n的副本。这条语句num1={number:30}在堆中创建了一个新对象,并将新对象的内存地址赋值给参数num1。注意在num1指向n之前,让我们测试验证一下://example1.jsletn={number:90}functionsum(num1){log(num1===n)num1={number:30}log(num1===n)}sum(n)$nodeexample1truefalse是的,我们是对的。就像我们在汇编代码中看到的一样。最初,num1引用与n相同的内存地址,因为n被压入堆栈。然后在创建对象后,将num1重新赋值给对象实例的内存地址。让我们进一步修改示例1:functionsum(num1){num1.number=30}letn={number:90}sum(n)//n变为{number:30}这将与前一个内存模型几乎相同和汇编语言。这里只有几处不同。在sum函数实现中,没有创建新对象,直接影响参数。...000270sum:000271mov(ebp+4),eax;//将参数值复制到eax寄存器中。eaxnowis0x002233000271mov30,[eax];//移动30到eax指向的地址num1为(ebp+4),包括n的地址。该值被复制到eax中,30被复制到eax指向的内存中。任何寄存器上的花括号[]告诉CPU不要使用在寄存器中找到的值,而是获取与其值对应的内存地址编号处的值。因此,检索到{number:90}值0x002233。看看这样的答案:原始数据类型按值传递,对象按引用复制传递。具体来说,当你传递一个对象(或数组)时,你是在无形中传递一个对该对象的引用,你可以修改对象的内容,但如果你试图覆盖该引用,它不会影响对象的副本-即引用本身是按值传递的:functionreplace(ref){ref={};//这段代码不影响传递的对象}functionupdate(ref){ref.key='newvalue';//这段代码不影响affectobject'scontent}vara={key:'value'};replace(a);//a还是原来的值,没有被修改update(a);//a的内容是我们汇编代码改的正如在内存模型中看到的那样。这个答案是绝对正确的。在replace函数内部,它在堆中创建一个新对象并将其分配给ref参数,一个对象的内存地址被覆盖。update函数引用ref参数中的内存地址,更改存储在内存地址的对象的key属性。总结根据我们上面所看到的,我们可以说原始数据类型和引用数据类型的副本作为参数传递给函数。不同之处在于,在原始数据类型中,它们仅由它们的实际值引用。JS 不允许我们获取它们的内存地址,不像在C、C++编程学习和实验系统中,引用数据类型是指它们的内存地址。
