You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
let setNewInt = function (i) {
i = i + 33;
};
let setNewString = function (str) {
str += "cool!";
};
let setNewArray = function (arr1) {
var b = [1, 2];
arr1 = b;
};
let setNewArrayElement = function (arr2) {
arr2[0] = 105;
};
let i = -33;
let str = "I am ";
let arr1 = [-4, -3];
let arr2 = [-19, 84];
console.log('i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);
setNewInt(i);
setNewString(str);
setNewArray(arr1);
setNewArrayElement(arr2);
console.log('现在, i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);
运行结果
i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: -19,84
现在, i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: 105,84
关于JavaScript如何将值传递给函数,在互联网上有很多误解和争论。大致认为,参数为原始数据类时使用按值传递,参数为数组、对象和函数等数据类型使用引用传递。
按值传递 和 引用传递参数 主要区别简单可以说:
但答案是 JavaScript 对所有数据类型都使用按值传递。它对数组和对象使用按值传递,但这是在的共享传参或拷贝的引用中使用的按值传参。这些说有些抽象,先来几个例子,接着,我们将研究JavaScript在 函数执行期间的内存模型,以了解实际发生了什么。
按值传参
在 JavaScript 中,原始类型的数据是按值传参;对象类型是跟Java一样,拷贝了原来对象的一份引用,对这个引用进行操作。但在 JS 中,string 就是一种原始类型数据而不是对象类。
运行结果
这边需要注意的两个地方:
**1)**第一个是通过
setNewString
方法把字符串str
传递进去,如果学过面向对象的语言如C#,Java 等,会认为调用这个方法后str
的值为改变,引用这在面向对象语言中是string
类型的是个对象,按引用传参,所以在这个方法里面更改str
外面也会跟着改变。但是 JavaScript 中就像前面所说,在JS 中,string 就是一种原始类型数据而不是对象类,所以是按值传递,所以在
setNewString
中更改str
的值不会影响到外面。**2)**第二个是通过
setNewArray
方法把数组arr1
传递进去,因为数组是对象类型,所以是引用传递,在这个方法里面我们更改 arr1 的指向,所以如果是这面向对象语言中,我们认为最后的结果arr1 的值是重新指向的那个,即[1, 2]
,但最后打印结果可以看出 arr1 的值还是原先的值,这是为什么呢?共享传递
Stack Overflow上Community Wiki 对上述的回答是:对于传递到函数参数的对象类型,如果直接改变了拷贝的引用的指向地址,那是不会影响到原来的那个对象;如果是通过拷贝的引用,去进行内部的值的操作,那么就会改变到原来的对象的。
可以参考博文 JavaScript Fundamentals (2) – Is JS call-by-value or call-by-reference?
缘由: 上述的
state1
相当于obj1
, 然后obj1.item = 'changed'
,对象obj1
内部的item
属性进行了改变,自然就影响到原对象obj1
。类似的,state2
也是就obj2
,在方法里 state2 指向了一个新的对象,也就是改变原有引用地址,这是不会影响到外面的对象(obj2),这种现象更专业的叫法:call-by-sharing,这边为了方便,暂且叫做 共享传递。内存模型
JavaScript 在执行期间为程序分配了三部分内存:代码区,调用堆栈和堆。 这些组合在一起称为程序的地址空间。
代码区:这是存储要执行的JS代码的区域。
调用堆::这个区域跟踪当前正在执行的函数,执行计算并存储局部变量。变量以后进先出法存储在堆栈中。最后一个进来的是第一个出去的,数值数据类型存储在这里。
例如:
在这里,变量
corn
和lion
值在执行期间存储在堆栈中。堆:是分配 JavaScript 引用数据类型(如对象)的地方。 与堆栈不同,内存分配是随机放置的,没有 LIFO策略。 为了防止堆中的内存漏洞,JS引擎有防止它们发生的内存管理器。
Here
,lion
和tiger
是引用类型,它们的值存储在堆中,并被推入堆栈。它们在堆栈中的值是堆中位置的内存地址。激活记录(Activation Record),参数传递
我们已经看到了 JS 程序的内存模型,现在,让我们看看在 JavaScript 中调用函数时会发生什么。
每当在 JS 中调用一个函数时,执行该函数所需的所有信息都放在堆栈上。这个信息就是所谓的激活记录(Activation Record)。
激活记录上的信息包括以下内容:
我们必须知道这一点,我们在
js
文件中编写的代码在执行之前由 JS 引擎(例如 V8,Rhino,SpiderMonke y等)编译为机器语言。所以以下的代码:
会被编译成如下机器码:
上面的代码是我们的js代码等价。 机器码和 JS 之间有一种语言,它是汇编语言。 JS 引擎中的代码生成器在最终生成机器码之前,首先是将 js 代码编译为汇编代码。
为了了解实际发生了什么,以及在函数调用期间如何将激活记录推入堆栈,我们必须了解程序是如何用汇编表示的。
为了跟踪函数调用期间参数是如何在 JS 中传递的,我们将例子一的代码使用汇编语言表示并跟踪其执行流程。
先介绍几个概念:
ESP:(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。与之对应的是 EBP(Extended Base Pointer),扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。
EBP:扩展基址指针寄存器(extended base pointer) 其内存放一个指针,该指针指向系统栈最上面一个栈帧的底部。
EBP 只是存取某时刻的 ESP,这个时刻就是进入一个函数内后,cpu 会将ESP的值赋给 EBP,此时就可以通过 EBP 对栈进行操作,比如获取函数参数,局部变量等,实际上使用 ESP 也可以。
我们看到
sum
函数有两个参数num1
和num2
。函数被调用,传入值分别为90
和100
的a
和b
。记住:值数据类型包含值,而引用数据类型包含内存地址。
在调用 sum 函数之前,将其参数推入堆栈
然后,它将返回地址推送到堆栈。返回地址存储在EIP 寄存器中:
接下来,它保存基指针
然后更改 EBP 并将调用保存寄存器推入堆栈。
为局部变量分配空间:
这里执行加法:
我们的返回值是190,把它赋给了 EAX。
然后,恢复所有寄存器值。
并将控制权返回给调用函数,推送到堆栈的参数被清除。
调用函数现在从 EAX 寄存器检索返回值到
s
的内存位置。我们已经看到了内存中发生了什么以及如何将参数传递汇编代码的函数。
调用函数之前,调用者将参数推入堆栈。因此,可以正确地说在 js 中传递参数是传入值的一份拷贝。如果被调用函数更改了参数的值,它不会影响原始值,因为它存储在其他地方,它只处理一个副本。
让我们看看传递引用数据类型时会发生什么。
用汇编代码表示:
我们在这里看到变量
n
保存了指向堆中其值的内存地址。 在sum
函数执行时,参数被推送到堆栈,由sum
函数接收。sum
函数创建另一个对象{number:30}
,它存储在另一个内存地址002231
中,并将其放在堆栈的参数位置。 将前面堆栈上的参数位置的对象{number:90}
的内存地址替换为新创建的对象{number:30}
的内存地址。这使得
n
保持不变。因此,复制引用策略是正确的。变量n
被推入堆栈,从而在sum
执行时成为n
的副本。此语句
num1 = {number:30}
在堆中创建了一个新对象,并将新对象的内存地址分配给参数num1
。 注意,在num1
指向n
之前,让我们进行测试以验证:是的,我们是对的。就像我们在汇编代码中看到的那样。最初,
num1
引用与n
相同的内存地址,因为n
被推入堆栈。然后在创建对象之后,将
num1
重新分配到对象实例的内存地址。让我们进一步修改我们的例子1:
这将具有与前一个几乎相同的内存模型和汇编语言。这里只有几件事不太一样。在
sum
函数实现中,没有新的对象创建,该参数受到直接影响。num1
是(ebp+4),包含n
的地址。值被复制到eax
中,30
被复制到eax
指向的内存中。任何寄存器上的花括号[]
都告诉 CPU 不要使用寄存器中找到的值,而是获取与其值对应的内存地址号的值。因此,检索0x002233
的{number: 90}
值。看看这样的答案:
原始数据类型按值传递,对象通过引用的副本传递。
具体来说,当你传递一个对象(或数组)时,你无形地传递对该对象的引用,并且可以修改该对象的内容,但是如果你尝试覆盖该引用,它将不会影响该对象的副本- 即引用本身按值传递:
从我们在汇编代码和内存模型中看到的。这个答案百分之百正确。在
replace
函数内部,它在堆中创建一个新对象,并将其分配给ref
参数,a
对象内存地址被重写。update
函数引用ref
参数中的内存地址,并更改存储在存储器地址中的对象的key
属性。##总结
根据我们上面看到的,我们可以说原始数据类型和引用数据类型的副本作为参数传递给函数。不同之处在于,在原始数据类型,它们只被它们的实际值引用。JS 不允许我们获取他们的内存地址,不像在C与C++程序设计学习与实验系统,引用数据类型指的是它们的内存地址。
原文:https://blog.bitsrc.io/master-javascript-call-by-sharing-parameter-passing-7049d65163ed
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
你的点赞是我持续分享好东西的动力,欢迎点赞!
欢迎加入前端大家庭,里面会经常分享一些技术资源。
The text was updated successfully, but these errors were encountered: