自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JavaScript是如何工作的:JavaScript的共享傳遞和按值傳遞

開發(fā) 前端
關(guān)于JavaScript如何將值傳遞給函數(shù),在互聯(lián)網(wǎng)上有很多誤解和爭論。大致認(rèn)為,參數(shù)為原始數(shù)據(jù)類時使用按值傳遞,參數(shù)為數(shù)組、對象和函數(shù)等數(shù)據(jù)類型使用引用傳遞。

 關(guān)于JavaScript如何將值傳遞給函數(shù),在互聯(lián)網(wǎng)上有很多誤解和爭論。大致認(rèn)為,參數(shù)為原始數(shù)據(jù)類時使用按值傳遞,參數(shù)為數(shù)組、對象和函數(shù)等數(shù)據(jù)類型使用引用傳遞。

按值傳遞 和 引用傳遞參數(shù) 主要區(qū)別簡單可以說:

  • 按值傳遞:在函數(shù)里面改變傳遞的值不會影響到外面
  • 引用傳遞:在函數(shù)里面改變傳遞的值會影響到外面

但答案是 JavaScript 對所有數(shù)據(jù)類型都使用按值傳遞。它對數(shù)組和對象使用按值傳遞,但這是在的共享傳參或拷貝的引用中使用的按值傳參。這些說有些抽象,先來幾個例子,接著,我們將研究JavaScript在 函數(shù)執(zhí)行期間的內(nèi)存模型,以了解實際發(fā)生了什么。

按值傳參

在 JavaScript 中,原始類型的數(shù)據(jù)是按值傳參;對象類型是跟Java一樣,拷貝了原來對象的一份引用,對這個引用進(jìn)行操作。但在 JS 中,string 就是一種原始類型數(shù)據(jù)而不是對象類。 

  1. let setNewInt = function (i) {  
  2. ii = i + 33;  
  3. };  
  4. let setNewString = function (str) {  
  5. str += "cool!";  
  6. };  
  7. let setNewArray = function (arr1) {  
  8. var b = [1, 2];  
  9. arr1 = b 
  10. };  
  11. let setNewArrayElement = function (arr2) {  
  12. arr2[0] = 105;  
  13. };  
  14. let i = -33;  
  15. let str = "I am " 
  16. let arr1 = [-4, -3];  
  17. let arr2 = [-19, 84];  
  18. console.log('i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);  
  19. setNewInt(i);  
  20. setNewString(str);  
  21. setNewArray(arr1);  
  22. setNewArrayElement(arr2);  
  23. console.log('現(xiàn)在, i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);  

運(yùn)行結(jié)果 

  1. i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: -19,84 
  2.  
  3. 現(xiàn)在, i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: 105,84  

這邊需要注意的兩個地方:

1)***個是通過 setNewString 方法把字符串 str 傳遞進(jìn)去,如果學(xué)過面向?qū)ο蟮恼Z言如C#,Java 等,會認(rèn)為調(diào)用這個方法后 str 的值為改變,引用這在面向?qū)ο笳Z言中是 string 類型的是個對象,按引用傳參,所以在這個方法里面更改 str 外面也會跟著改變。

但是 JavaScript 中就像前面所說,在JS 中,string 就是一種原始類型數(shù)據(jù)而不是對象類,所以是按值傳遞,所以在 setNewString 中更改 str 的值不會影響到外面。

2)第二個是通過 setNewArray 方法把數(shù)組 arr1 傳遞進(jìn)去,因為數(shù)組是對象類型,所以是引用傳遞,在這個方法里面我們更改 arr1 的指向,所以如果是這面向?qū)ο笳Z言中,我們認(rèn)為***的結(jié)果arr1 的值是重新指向的那個,即 [1, 2],但***打印結(jié)果可以看出 arr1 的值還是原先的值,這是為什么呢?

共享傳遞

Stack Overflow上Community Wiki 對上述的回答是:對于傳遞到函數(shù)參數(shù)的對象類型,如果直接改變了拷貝的引用的指向地址,那是不會影響到原來的那個對象;如果是通過拷貝的引用,去進(jìn)行內(nèi)部的值的操作,那么就會改變到原來的對象的。

可以參考博文 JavaScript Fundamentals (2) – Is JS call-by-value or call-by-reference? 

  1. function changeStuff(state1, state2)  
  2.  
  3. state1.item = 'changed' 
  4. state2 = {item: "changed"};  
  5.  
  6. var obj1 = {item: "unchanged"};  
  7. var obj2 = {item: "unchanged"};  
  8. changeStuff(obj1, obj2);  
  9. console.log(obj1.item); // obj1.item 會被改變  
  10. console.log(obj2.item); // obj2.item 不會被改變  

緣由: 上述的 state1 相當(dāng)于 obj1, 然后 obj1.item = 'changed',對象 obj1 內(nèi)部的 item 屬性進(jìn)行了改變,自然就影響到原對象 obj1 。類似的,state2 也是就 obj2,在方法里 state2 指向了一個新的對象,也就是改變原有引用地址,這是不會影響到外面的對象(obj2),這種現(xiàn)象更專業(yè)的叫法:call-by-sharing,這邊為了方便,暫且叫做 共享傳遞。

內(nèi)存模型

JavaScript 在執(zhí)行期間為程序分配了三部分內(nèi)存:代碼區(qū),調(diào)用堆棧和堆。 這些組合在一起稱為程序的地址空間。

 

代碼區(qū):這是存儲要執(zhí)行的JS代碼的區(qū)域。

調(diào)用堆::這個區(qū)域跟蹤當(dāng)前正在執(zhí)行的函數(shù),執(zhí)行計算并存儲局部變量。變量以后進(jìn)先出法存儲在堆棧中。***一個進(jìn)來的是***個出去的,數(shù)值數(shù)據(jù)類型存儲在這里。

例如: 

  1. var corn = 95  
  2. let lion = 100 

 

在這里,變量 corn 和 lion 值在執(zhí)行期間存儲在堆棧中。

堆:是分配 JavaScript 引用數(shù)據(jù)類型(如對象)的地方。 與堆棧不同,內(nèi)存分配是隨機(jī)放置的,沒有 LIFO策略。 為了防止堆中的內(nèi)存漏洞,JS引擎有防止它們發(fā)生的內(nèi)存管理器。 

  1. class Animal {}  
  2. // 在內(nèi)存地址 0x001232 上存儲 new Animal() 實例  
  3. // tiger 的堆棧值為 0x001232  
  4. const tiger = new Animal()  
  5. // 在內(nèi)存地址 0x000001 上存儲 new Objec實例  
  6. // `lion` 的堆棧值為 0x000001  
  7. let lion = {  
  8. strength: "Very Strong"  
  9.    
   

Here,lion 和 tiger 是引用類型,它們的值存儲在堆中,并被推入堆棧。它們在堆棧中的值是堆中位置的內(nèi)存地址。

激活記錄(Activation Record),參數(shù)傳遞

我們已經(jīng)看到了 JS 程序的內(nèi)存模型,現(xiàn)在,讓我們看看在 JavaScript 中調(diào)用函數(shù)時會發(fā)生什么。 

  1. // 例子一  
  2. function sum(num1,num2) {  
  3. var result = num1 + num2  
  4. return result  
  5.  
  6. var a = 90  
  7. var b = 100  
  8. sum(a, b)  

每當(dāng)在 JS 中調(diào)用一個函數(shù)時,執(zhí)行該函數(shù)所需的所有信息都放在堆棧上。這個信息就是所謂的激活記錄(Activation Record)。

這個 Activation Record,我直譯為激活記錄,找了好多資料,沒有看到中文一個比較好的翻譯,如果朋友們知道,歡迎留言。

激活記錄上的信息包括以下內(nèi)容:

  • SP 堆棧指針:調(diào)用方法之前堆棧指針的當(dāng)前位置。
  • RA 返回地址:這是函數(shù)執(zhí)行完成后繼續(xù)執(zhí)行的地址。
  • RV 返回值:這是可選的,函數(shù)可以返回值,也可以不返回值。
  • 參數(shù):將函數(shù)所需的參數(shù)推入堆棧。
  • 局部變量:函數(shù)使用的變量被推送到堆棧。

我們必須知道這一點(diǎn),我們在js文件中編寫的代碼在執(zhí)行之前由 JS 引擎(例如 V8,Rhino,SpiderMonke y等)編譯為機(jī)器語言。

所以以下的代碼: 

  1. let shark = "Sea Animal" 

會被編譯成如下機(jī)器碼: 

  1. 01000100101010  
  2. 01010101010101  

上面的代碼是我們的js代碼等價。 機(jī)器碼和 JS 之間有一種語言,它是匯編語言。 JS 引擎中的代碼生成器在最終生成機(jī)器碼之前,首先是將 js 代碼編譯為匯編代碼。

為了了解實際發(fā)生了什么,以及在函數(shù)調(diào)用期間如何將激活記錄推入堆棧,我們必須了解程序是如何用匯編表示的。

為了跟蹤函數(shù)調(diào)用期間參數(shù)是如何在 JS 中傳遞的,我們將例子一的代碼使用匯編語言表示并跟蹤其執(zhí)行流程。

先介紹幾個概念:

ESP:(Extended Stack Pointer)為擴(kuò)展棧指針寄存器,是指針寄存器的一種,用于存放函數(shù)棧頂指針。與之對應(yīng)的是 EBP(Extended Base Pointer),擴(kuò)展基址指針寄存器,也被稱為幀指針寄存器,用于存放函數(shù)棧底指針。

EBP:擴(kuò)展基址指針寄存器(extended base pointer) 其內(nèi)存放一個指針,該指針指向系統(tǒng)棧最上面一個棧幀的底部。

EBP 只是存取某時刻的 ESP,這個時刻就是進(jìn)入一個函數(shù)內(nèi)后,cpu 會將ESP的值賦給 EBP,此時就可以通過 EBP 對棧進(jìn)行操作,比如獲取函數(shù)參數(shù),局部變量等,實際上使用 ESP 也可以。 

  1. // 例子一  
  2. function sum(num1,num2) {  
  3. var result = num1 + num2  
  4. return result  
  5.  
  6. var a = 90  
  7. var b = 100  
  8. var s = sum(a, b)  

我們看到 sum 函數(shù)有兩個參數(shù) num1 和 num2。函數(shù)被調(diào)用,傳入值分別為 90 和 100 的 a 和 b。

記住:值數(shù)據(jù)類型包含值,而引用數(shù)據(jù)類型包含內(nèi)存地址。

在調(diào)用 sum 函數(shù)之前,將其參數(shù)推入堆棧 

  1. ESP->[......]  
  2. ESP->[ 100 ]  
  3. [ 90 ]  
  4. [.......]  

然后,它將返回地址推送到堆棧。返回地址存儲在EIP 寄存器中: 

  1. ESP->[Old EIP]  
  2. [ 100 ]  
  3. [ 90 ]  
  4. [.......]  

接下來,它保存基指針 

  1. ESP->[Old EBP]  
  2. [Old EIP]  
  3. [ 100 ]  
  4. [ 90 ]  
  5. [.......]  

然后更改 EBP 并將調(diào)用保存寄存器推入堆棧。 

  1. ESP->[Old ESI]  
  2. [Old EBX]  
  3. [Old EDI]  
  4. EBP->[Old EBP]  
  5. [Old EIP]  
  6. [ 100 ]  
  7. [ 90 ]  
  8. [.......]  

為局部變量分配空間: 

  1. ESP->[ ]  
  2. [Old ESI]  
  3. [Old EBX]  
  4. [Old EDI]  
  5. EBP->[Old EBP]  
  6. [Old EIP]  
  7. [ 100 ]  
  8. [ 90 ]  
  9. [.......]  

這里執(zhí)行加法: 

  1. mov ebp+4, eax ; 100  
  2. add ebp+8, eax ; eaxeax = eax + (ebp+8)  
  3. mov eax, ebp+16  
  4. ESP->[ 190 ]  
  5. [Old ESI]  
  6. [Old EBX]  
  7. [Old EDI]  
  8. EBP->[Old EBP]  
  9. [Old EIP]  
  10. [ 100 ]  
  11. [ 90 ]  
  12. [.......]  

我們的返回值是190,把它賦給了 EAX。 

  1. mov ebp+16, eax 

EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。

然后,恢復(fù)所有寄存器值。 

  1. [ 190 ] DELETED  
  2. [Old ESI] DELETED  
  3. [Old EBX] DELETED  
  4. [Old EDI] DELETED  
  5. [Old EBP] DELETED  
  6. [Old EIP] DELETED  
  7. ESP->[ 100 ]  
  8. [ 90 ]  
  9. EBP->[.......]  

并將控制權(quán)返回給調(diào)用函數(shù),推送到堆棧的參數(shù)被清除。 

  1. [ 190 ] DELETED  
  2. [Old ESI] DELETED  
  3. [Old EBX] DELETED  
  4. [Old EDI] DELETED  
  5. [Old EBP] DELETED  
  6. [Old EIP] DELETED  
  7. [ 100 ] DELETED  
  8. [ 90 ] DELETED  
  9. [ESP, EBP]->[.......]  

調(diào)用函數(shù)現(xiàn)在從 EAX 寄存器檢索返回值到 s 的內(nèi)存位置。 

  1. mov eax, 0x000002 ; // s 變量在內(nèi)存中的位置 

我們已經(jīng)看到了內(nèi)存中發(fā)生了什么以及如何將參數(shù)傳遞匯編代碼的函數(shù)。

調(diào)用函數(shù)之前,調(diào)用者將參數(shù)推入堆棧。因此,可以正確地說在 js 中傳遞參數(shù)是傳入值的一份拷貝。如果被調(diào)用函數(shù)更改了參數(shù)的值,它不會影響原始值,因為它存儲在其他地方,它只處理一個副本。 

  1. function sum(num1) {  
  2. num1 = 30  
  3.  
  4. let n = 90  
  5. sum(n)  
  6. // `n` 仍然為 90  

讓我們看看傳遞引用數(shù)據(jù)類型時會發(fā)生什么。 

  1. function sum(num1) {  
  2. num1 = { number:30 }  
  3.  
  4. let n = { number:90 }  
  5. sum(n)  
  6. // `n` 仍然是 { number:90 }  

用匯編代碼表示: 

  1. n -> 0x002233  
  2. Heap: Stack:  
  3. 002254 012222  
  4. ... 012223 0x002233  
  5. 002240 012224  
  6. 002239 012225  
  7. 002238  
  8. 002237  
  9. 002236  
  10. 002235  
  11. 002234  
  12. 002233 { number: 90 }  
  13. 002232  
  14. 002231 { number: 30 }  
  15. Code:  
  16. ...  
  17. 000233 main: // entry point  
  18. 000234 push n // n 值為 002233 ,它指向堆中存放 {number: 90} 地址。 n 被推到堆棧的 0x12223 處.  
  19. 000235 ; // 保存所有寄存器  
  20. ...  
  21. 000239 call sum ; // 跳轉(zhuǎn)到內(nèi)存中的`sum`函數(shù)  
  22. 000240  
  23. ...  
  24. 000270 sum:  
  25. 000271 ; // 創(chuàng)建對象 {number: 30} 內(nèi)在地址主 0x002231  
  26. 000271 mov 0x002231, (ebp+4) ; // 將內(nèi)存地址為 0x002231 中 {number: 30} 移動到堆棧 (ebp+4)。(ebp+4)是地址 0x12223 ,即 n 所在地址也是對象 {number: 90} 在堆中的位置。這里,堆棧位置被值 0x002231 覆蓋?,F(xiàn)在,num1 指向另一個內(nèi)存地址。 
  27. 000272 ; // 清理堆棧  
  28. ...  
  29. 000275 ret ; // 回到調(diào)用者所在的位置(000240)  

我們在這里看到變量n保存了指向堆中其值的內(nèi)存地址。 在sum 函數(shù)執(zhí)行時,參數(shù)被推送到堆棧,由 sum 函數(shù)接收。

sum 函數(shù)創(chuàng)建另一個對象 {number:30},它存儲在另一個內(nèi)存地址 002231 中,并將其放在堆棧的參數(shù)位置。 將前面堆棧上的參數(shù)位置的對象 {number:90} 的內(nèi)存地址替換為新創(chuàng)建的對象 {number:30} 的內(nèi)存地址。

這使得 n 保持不變。因此,復(fù)制引用策略是正確的。變量 n 被推入堆棧,從而在 sum 執(zhí)行時成為 n 的副本。

此語句 num1 = {number:30} 在堆中創(chuàng)建了一個新對象,并將新對象的內(nèi)存地址分配給參數(shù) num1。 注意,在 num1 指向 n 之前,讓我們進(jìn)行測試以驗證: 

  1. // example1.js  
  2. let n = { number: 90 }  
  3. function sum(num1) {  
  4. log(num1 === n)  
  5. num1 = { number: 30 }  
  6. log(num1 === n)  
  7.  
  8. sum(n)  
  9. $ node example1  
  10. true  
  11. false  

是的,我們是對的。就像我們在匯編代碼中看到的那樣。最初,num1 引用與 n 相同的內(nèi)存地址,因為n被推入堆棧。

然后在創(chuàng)建對象之后,將 num1 重新分配到對象實例的內(nèi)存地址。

讓我們進(jìn)一步修改我們的例子1: 

  1. function sum(num1) {  
  2. num1.number = 30  
  3.  
  4. let n = { number: 90 }  
  5. sum(n)  
  6. // n 成為了 { number: 30 }  

這將具有與前一個幾乎相同的內(nèi)存模型和匯編語言。這里只有幾件事不太一樣。在 sum 函數(shù)實現(xiàn)中,沒有新的對象創(chuàng)建,該參數(shù)受到直接影響。 

  1. ...  
  2. 000270 sum:  
  3. 000271 mov (ebp+4), eax ; // 將參數(shù)值復(fù)制到 eax 寄存器。eax 現(xiàn)在為 0x002233  
  4. 000271 mov 30, [eax]; // 將 30 移動到 eax 指向的地址  

num1 是(ebp+4),包含 n 的地址。值被復(fù)制到 eax 中,30 被復(fù)制到 eax 指向的內(nèi)存中。任何寄存器上的花括號 [] 都告訴 CPU 不要使用寄存器中找到的值,而是獲取與其值對應(yīng)的內(nèi)存地址號的值。因此,檢索 0x002233 的 {number: 90} 值。

看看這樣的答案:

原始數(shù)據(jù)類型按值傳遞,對象通過引用的副本傳遞。

具體來說,當(dāng)你傳遞一個對象(或數(shù)組)時,你無形地傳遞對該對象的引用,并且可以修改該對象的內(nèi)容,但是如果你嘗試覆蓋該引用,它將不會影響該對象的副本- 即引用本身按值傳遞: 

  1. function replace(ref) {  
  2. ref = {}; // 這段代碼不影響傳遞的對象  
  3.  
  4. function update(ref) {  
  5. ref.key = 'newvalue'; // 這段代碼確實會影響對象的內(nèi)容  
  6.  
  7. var a = { key: 'value' };  
  8. replace(a); // a 仍然有其原始值,它沒有被修改的  
  9. update(a); // a 的內(nèi)容被更改  

從我們在匯編代碼和內(nèi)存模型中看到的。這個答案***正確。在 replace 函數(shù)內(nèi)部,它在堆中創(chuàng)建一個新對象,并將其分配給 ref 參數(shù),a 對象內(nèi)存地址被重寫。

update 函數(shù)引用 ref 參數(shù)中的內(nèi)存地址,并更改存儲在存儲器地址中的對象的key屬性。

總結(jié)

根據(jù)我們上面看到的,我們可以說原始數(shù)據(jù)類型和引用數(shù)據(jù)類型的副本作為參數(shù)傳遞給函數(shù)。不同之處在于,在原始數(shù)據(jù)類型,它們只被它們的實際值引用。JS 不允許我們獲取他們的內(nèi)存地址,不像在C與C++程序設(shè)計學(xué)習(xí)與實驗系統(tǒng),引用數(shù)據(jù)類型指的是它們的內(nèi)存地址。

責(zé)任編輯:龐桂玉 來源: segmentfault
相關(guān)推薦

2015-09-08 10:16:41

Java參數(shù)按值傳遞

2009-06-09 21:54:26

傳遞參數(shù)JavaScript

2022-07-29 08:05:31

Java值傳遞

2017-12-05 08:53:20

Golang參數(shù)傳遞

2022-05-18 08:00:00

JavaScriptFetch數(shù)據(jù)

2011-03-15 13:45:49

JavaScript后續(xù)傳遞

2023-11-15 09:14:27

Java值傳遞

2017-11-16 14:25:04

WOT峰會

2019-01-14 08:06:37

JavaScript

2018-12-13 14:10:37

JavaScript調(diào)用堆棧前端

2011-03-25 13:44:28

Java值傳遞

2020-09-02 08:00:51

Java引用傳遞值傳遞

2022-11-02 15:00:03

Java值傳遞引用傳遞

2019-07-22 15:29:53

JavaScriptGitHub語言

2019-06-04 14:15:08

JavaScript V8前端

2024-09-04 01:36:51

Java對象傳遞

2012-02-21 14:04:15

Java

2010-01-06 13:51:15

Javascript傳

2016-09-18 19:07:33

Java值傳遞引用傳遞

2012-05-07 13:23:47

ASP.NET
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號