摘要:它對數組和對象使用按值傳遞,但這是在的共享傳參或拷貝的引用中使用的按值傳參。例如在這里,變量和值在執行期間存儲在堆棧中。返回值這是可選的,函數可以返回值,也可以不返回值。變量被推入堆棧,從而在執行時成為的副本。
這是專門探索 JavaScript 及其所構建的組件的系列文章的第 22 篇。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
如果你錯過了前面的章節,可以在這里找到它們:
JavaScript 是如何工作的:引擎,運行時和調用堆棧的概述!
JavaScript 是如何工作的:深入V8引擎&編寫優化代碼的5個技巧!
JavaScript 是如何工作的:內存管理+如何處理4個常見的內存泄漏!
JavaScript 是如何工作的:事件循環和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
JavaScript 是如何工作的:深入探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
JavaScript 是如何工作的:與 WebAssembly比較 及其使用場景!
JavaScript 是如何工作的:Web Workers的構建塊+ 5個使用他們的場景!
JavaScript 是如何工作的:Service Worker 的生命周期及使用場景!
JavaScript 是如何工作的:Web 推送通知的機制!
JavaScript 是如何工作的:使用 MutationObserver 跟蹤 DOM 的變化!
JavaScript 是如何工作的:渲染引擎和優化其性能的技巧!
JavaScript 是如何工作的:深入網絡層 + 如何優化性能和安全!
JavaScript 是如何工作的:CSS 和 JS 動畫底層原理及如何優化它們的性能!
JavaScript 是如何工作的:解析、抽象語法樹(AST)+ 提升編譯速度5個技巧!
JavaScript 是如何工作的:深入類和繼承內部原理+Babel和 TypeScript 之間轉換!
JavaScript 是如何工作的:存儲引擎+如何選擇合適的存儲API!
JavaScript 是如何工作的:Shadow DOM 的內部結構+如何編寫獨立的組件!
JavaScript 是如何工作的:WebRTC 和對等網絡的機制!
JavaScript 是如何工作的:編寫自己的 Web 開發框架 + React 及其虛擬 DOM 原理!
JavaScript 是如何工作的:模塊的構建以及對應的打包工具
JavaScript 是如何工作的:JavaScript 的內存模型
關于JavaScript如何將值傳遞給函數,在互聯網上有很多誤解和爭論。大致認為,參數為原始數據類時使用按值傳遞,參數為數組、對象和函數等數據類型使用引用傳遞。
按值傳遞 和 引用傳遞參數 主要區別簡單可以說:
按值傳遞:在函數里面改變傳遞的值不會影響到外面
引用傳遞:在函數里面改變傳遞的值會影響到外面
但答案是 JavaScript 對所有數據類型都使用按值傳遞。它對數組和對象使用按值傳遞,但這是在的共享傳參或拷貝的引用中使用的按值傳參。這些說有些抽象,先來幾個例子,接著,我們將研究JavaScript在 函數執行期間的內存模型,以了解實際發生了什么。
按值傳參在 JavaScript 中,原始類型的數據是按值傳參;對象類型是跟Java一樣,拷貝了原來對象的一份引用,對這個引用進行操作。但在 JS 中,string 就是一種原始類型數據而不是對象類。
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
這邊需要注意的兩個地方:
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?
function changeStuff(state1, state2) { state1.item = "changed"; state2 = {item: "changed"}; } var obj1 = {item: "unchanged"}; var obj2 = {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,這邊為了方便,暫且叫做 共享傳遞。
內存模型JavaScript 在執行期間為程序分配了三部分內存:代碼區,調用堆棧和堆。 這些組合在一起稱為程序的地址空間。
代碼區:這是存儲要執行的JS代碼的區域。
調用堆::這個區域跟蹤當前正在執行的函數,執行計算并存儲局部變量。變量以后進先出法存儲在堆棧中。最后一個進來的是第一個出去的,數值數據類型存儲在這里。
例如:
var corn = 95 let lion = 100
在這里,變量 corn 和 lion 值在執行期間存儲在堆棧中。
堆:是分配 JavaScript 引用數據類型(如對象)的地方。 與堆棧不同,內存分配是隨機放置的,沒有 LIFO策略。 為了防止堆中的內存漏洞,JS引擎有防止它們發生的內存管理器。
class Animal {} // 在內存地址 0x001232 上存儲 new Animal() 實例 // tiger 的堆棧值為 0x001232 const tiger = new Animal() // 在內存地址 0x000001 上存儲 new Objec實例 // `lion` 的堆棧值為 0x000001 let lion = { strength: "Very Strong" }
Here,lion 和 tiger 是引用類型,它們的值存儲在堆中,并被推入堆棧。它們在堆棧中的值是堆中位置的內存地址。
激活記錄(Activation Record),參數傳遞我們已經看到了 JS 程序的內存模型,現在,讓我們看看在 JavaScript 中調用函數時會發生什么。
// 例子一 function sum(num1,num2) { var result = num1 + num2 return result } var a = 90 var b = 100 sum(a, b)
每當在 JS 中調用一個函數時,執行該函數所需的所有信息都放在堆棧上。這個信息就是所謂的激活記錄(Activation Record)。
這個 Activation Record,我直譯為激活記錄,找了好多資料,沒有看到中文一個比較好的翻譯,如果朋友們知道,歡迎留言。
激活記錄上的信息包括以下內容:
SP 堆棧指針:調用方法之前堆棧指針的當前位置。
RA 返回地址:這是函數執行完成后繼續執行的地址。
RV 返回值:這是可選的,函數可以返回值,也可以不返回值。
參數:將函數所需的參數推入堆棧。
局部變量:函數使用的變量被推送到堆棧。
我們必須知道這一點,我們在js文件中編寫的代碼在執行之前由 JS 引擎(例如 V8,Rhino,SpiderMonke y等)編譯為機器語言。
所以以下的代碼:
let shark = "Sea Animal"
會被編譯成如下機器碼:
01000100101010 01010101010101
上面的代碼是我們的js代碼等價。 機器碼和 JS 之間有一種語言,它是匯編語言。 JS 引擎中的代碼生成器在最終生成機器碼之前,首先是將 js 代碼編譯為匯編代碼。
為了了解實際發生了什么,以及在函數調用期間如何將激活記錄推入堆棧,我們必須了解程序是如何用匯編表示的。
為了跟蹤函數調用期間參數是如何在 JS 中傳遞的,我們將例子一的代碼使用匯編語言表示并跟蹤其執行流程。
先介紹幾個概念:
ESP:(Extended Stack Pointer)為擴展棧指針寄存器,是指針寄存器的一種,用于存放函數棧頂指針。與之對應的是 EBP(Extended Base Pointer),擴展基址指針寄存器,也被稱為幀指針寄存器,用于存放函數棧底指針。
EBP:擴展基址指針寄存器(extended base pointer) 其內存放一個指針,該指針指向系統棧最上面一個棧幀的底部。
EBP 只是存取某時刻的 ESP,這個時刻就是進入一個函數內后,cpu 會將ESP的值賦給 EBP,此時就可以通過 EBP 對棧進行操作,比如獲取函數參數,局部變量等,實際上使用 ESP 也可以。
// 例子一 function sum(num1,num2) { var result = num1 + num2 return result } var a = 90 var b = 100 var s = sum(a, b)
我們看到 sum 函數有兩個參數 num1 和 num2。函數被調用,傳入值分別為 90 和 100 的 a 和 b。
記住:值數據類型包含值,而引用數據類型包含內存地址。
在調用 sum 函數之前,將其參數推入堆棧
ESP->[......] ESP->[ 100 ] [ 90 ] [.......]
然后,它將返回地址推送到堆棧。返回地址存儲在EIP 寄存器中:
ESP->[Old EIP] [ 100 ] [ 90 ] [.......]
接下來,它保存基指針
ESP->[Old EBP] [Old EIP] [ 100 ] [ 90 ] [.......]
然后更改 EBP 并將調用保存寄存器推入堆棧。
ESP->[Old ESI] [Old EBX] [Old EDI] EBP->[Old EBP] [Old EIP] [ 100 ] [ 90 ] [.......]
為局部變量分配空間:
ESP->[ ] [Old ESI] [Old EBX] [Old EDI] EBP->[Old EBP] [Old EIP] [ 100 ] [ 90 ] [.......]
這里執行加法:
mov ebp+4, eax ; 100 add ebp+8, eax ; eax = eax + (ebp+8) mov eax, ebp+16 ESP->[ 190 ] [Old ESI] [Old EBX] [Old EDI] EBP->[Old EBP] [Old EIP] [ 100 ] [ 90 ] [.......]
我們的返回值是190,把它賦給了 EAX。
mov ebp+16, eax
EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
然后,恢復所有寄存器值。
[ 190 ] DELETED [Old ESI] DELETED [Old EBX] DELETED [Old EDI] DELETED [Old EBP] DELETED [Old EIP] DELETED ESP->[ 100 ] [ 90 ] EBP->[.......]
并將控制權返回給調用函數,推送到堆棧的參數被清除。
[ 190 ] DELETED [Old ESI] DELETED [Old EBX] DELETED [Old EDI] DELETED [Old EBP] DELETED [Old EIP] DELETED [ 100 ] DELETED [ 90 ] DELETED [ESP, EBP]->[.......]
調用函數現在從 EAX 寄存器檢索返回值到 s 的內存位置。
mov eax, 0x000002 ; // s 變量在內存中的位置
我們已經看到了內存中發生了什么以及如何將參數傳遞匯編代碼的函數。
調用函數之前,調用者將參數推入堆棧。因此,可以正確地說在 js 中傳遞參數是傳入值的一份拷貝。如果被調用函數更改了參數的值,它不會影響原始值,因為它存儲在其他地方,它只處理一個副本。
function sum(num1) { num1 = 30 } let n = 90 sum(n) // `n` 仍然為 90
讓我們看看傳遞引用數據類型時會發生什么。
function sum(num1) { num1 = { number:30 } } let n = { number:90 } sum(n) // `n` 仍然是 { number:90 }
用匯編代碼表示:
n -> 0x002233 Heap: Stack: 002254 012222 ... 012223 0x002233 002240 012224 002239 012225 002238 002237 002236 002235 002234 002233 { number: 90 } 002232 002231 { number: 30 } Code: ... 000233 main: // entry point 000234 push n // n 值為 002233 ,它指向堆中存放 {number: 90} 地址。 n 被推到堆棧的 0x12223 處. 000235 ; // 保存所有寄存器 ... 000239 call sum ; // 跳轉到內存中的`sum`函數 000240 ... 000270 sum: 000271 ; // 創建對象 {number: 30} 內在地址主 0x002231 000271 mov 0x002231, (ebp+4) ; // 將內存地址為 0x002231 中 {number: 30} 移動到堆棧 (ebp+4)。(ebp+4)是地址 0x12223 ,即 n 所在地址也是對象 {number: 90} 在堆中的位置。這里,堆棧位置被值 0x002231 覆蓋。現在,num1 指向另一個內存地址。 000272 ; // 清理堆棧 ... 000275 ret ; // 回到調用者所在的位置(000240)
我們在這里看到變量n保存了指向堆中其值的內存地址。 在sum 函數執行時,參數被推送到堆棧,由 sum 函數接收。
sum 函數創建另一個對象 {number:30},它存儲在另一個內存地址 002231 中,并將其放在堆棧的參數位置。 將前面堆棧上的參數位置的對象 {number:90} 的內存地址替換為新創建的對象 {number:30} 的內存地址。
這使得 n 保持不變。因此,復制引用策略是正確的。變量 n 被推入堆棧,從而在 sum 執行時成為 n 的副本。
此語句 num1 = {number:30} 在堆中創建了一個新對象,并將新對象的內存地址分配給參數 num1。 注意,在 num1 指向 n 之前,讓我們進行測試以驗證:
// example1.js let n = { number: 90 } function sum(num1) { log(num1 === n) num1 = { number: 30 } log(num1 === n) } sum(n) $ node example1 true false
是的,我們是對的。就像我們在匯編代碼中看到的那樣。最初,num1 引用與 n 相同的內存地址,因為n被推入堆棧。
然后在創建對象之后,將 num1 重新分配到對象實例的內存地址。
讓我們進一步修改我們的例子1:
function sum(num1) { num1.number = 30 } let n = { number: 90 } sum(n) // n 成為了 { number: 30 }
這將具有與前一個幾乎相同的內存模型和匯編語言。這里只有幾件事不太一樣。在 sum 函數實現中,沒有新的對象創建,該參數受到直接影響。
... 000270 sum: 000271 mov (ebp+4), eax ; // 將參數值復制到 eax 寄存器。eax 現在為 0x002233 000271 mov 30, [eax]; // 將 30 移動到 eax 指向的地址
num1 是(ebp+4),包含 n 的地址。值被復制到 eax 中,30 被復制到 eax 指向的內存中。任何寄存器上的花括號 [] 都告訴 CPU 不要使用寄存器中找到的值,而是獲取與其值對應的內存地址號的值。因此,檢索 0x002233 的 {number: 90} 值。
看看這樣的答案:
原始數據類型按值傳遞,對象通過引用的副本傳遞。
具體來說,當你傳遞一個對象(或數組)時,你無形地傳遞對該對象的引用,并且可以修改該對象的內容,但是如果你嘗試覆蓋該引用,它將不會影響該對象的副本- 即引用本身按值傳遞:
function replace(ref) { ref = {}; // 這段代碼不影響傳遞的對象 } function update(ref) { ref.key = "newvalue"; // 這段代碼確實會影響對象的內容 } var a = { key: "value" }; replace(a); // a 仍然有其原始值,它沒有被修改的 update(a); // a 的內容被更改
從我們在匯編代碼和內存模型中看到的。這個答案百分之百正確。在 replace 函數內部,它在堆中創建一個新對象,并將其分配給 ref 參數,a 對象內存地址被重寫。
update 函數引用 ref 參數中的內存地址,并更改存儲在存儲器地址中的對象的key屬性。
總結根據我們上面看到的,我們可以說原始數據類型和引用數據類型的副本作為參數傳遞給函數。不同之處在于,在原始數據類型,它們只被它們的實際值引用。JS 不允許我們獲取他們的內存地址,不像在C與C++程序設計學習與實驗系統,引用數據類型指的是它們的內存地址。
原文:https://blog.bitsrc.io/master...
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
你的點贊是我持續分享好東西的動力,歡迎點贊!
交流干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。
https://github.com/qq44924588...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74117.html
摘要:它對數組和對象使用按值傳遞,但這是在的共享傳參或拷貝的引用中使用的按值傳參。例如在這里,變量和值在執行期間存儲在堆棧中。返回值這是可選的,函數可以返回值,也可以不返回值。變量被推入堆棧,從而在執行時成為的副本。 這是專門探索 JavaScript 及其所構建的組件的系列文章的第 22 篇。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 如果你錯過了前面的章節,可...
摘要:中所有函數的參數都是按值傳遞的。基本類型本身是按值傳遞,具有不可變性,對基本類型的修改,實質上都是在棧內存中創建了新的值。中把這種拷貝也認為是按值傳遞。 本文共 1200 字,讀完只需 5 分鐘 概述 參數的傳遞分為按值傳遞和按引用傳遞,而 JavaScript 中參數的傳遞只有按值傳遞。 ECMAScript 中所有函數的參數都是按值傳遞的。 所謂按值傳遞就是: 把函數外部的值復制給...
摘要:最早由在年的語言中提出。該求值策略被用于等多種語言。該策略的重點是調用函數傳參時,函數接受對象實參引用的副本既不是按值傳遞的對象副本,也不是按引用傳遞的隱式引用。它和按引用傳遞的不同在于在共享傳遞中對函數形參的賦值,不會影響實參的值。 周五晚上下班回家的路上,突然想到了 CommonJS 規范、Node.js 模塊化等等各種東西,然后就想到了熟悉的 module.exports。 大約...
摘要:為了方便大家共同學習,整理了之前博客系列的文章,目前已整理是如何工作這個系列,可以請猛戳博客查看。以下列出該系列目錄,歡迎點個星星,我將更友動力整理理優質的文章,一起學習。 為了方便大家共同學習,整理了之前博客系列的文章,目前已整理 JavaScript 是如何工作這個系列,可以請猛戳GitHub博客查看。 以下列出該系列目錄,歡迎點個星星,我將更友動力整理理優質的文章,一起學習。 J...
摘要:按引用傳遞時,函數的形參接收實參的隱式引用,而不再是副本。探究值的傳遞方式的基本類型,是按值傳遞的。但這樣是否說明的對象是按引用傳遞的呢我們再看下面的例子仍然是并未被修改為如果是按引用傳遞,修改形參的值,應該影響到實參才對。 最近遇到個有趣的問題:JS中的值是按值傳遞,還是按引用傳遞呢? 在分析這個問題之前,我們需了解什么是按值傳遞(call by value),什么是按引用傳遞(ca...
閱讀 3152·2021-11-04 16:09
閱讀 3106·2021-09-23 11:49
閱讀 3603·2021-09-09 09:33
閱讀 3604·2021-08-18 10:22
閱讀 2041·2019-08-30 15:55
閱讀 3624·2019-08-30 15:53
閱讀 2653·2019-08-28 18:08
閱讀 888·2019-08-26 18:18