一篇文中告訴你JS中的"值傳遞"和"引用傳遞"
前言
現代的前端開發,不再是刀耕火種的 JQ 時代,而是 MVVM ,組件化,工程化,承載著日益復雜的業務邏輯。內存消耗和性能問題,成為當代開發者必須要考慮的問題。
本文從堆棧內存講起,讓大傢理解JS中變量的內存使用以及變動情況 。
初步瞭解堆棧
先初步瞭解JS中的堆和棧,內存空間分為 堆和棧 兩個區域,代碼運行時,解析器會先判斷變量類型,根據變量類型,將變量放到不同的內存空間中(堆和棧)。
如圖所示
堆棧和類型的關系
基本的數據類型(String,Number,Boolean,Null,Undefined, Symbol)都會分配棧區。它的值是存放在棧中的簡單數據段,數據大小確定,內存空間大小可以分配;按值存放,所以可以按值訪問。
引用數據類型 Object (對象)的變量都放到堆區。它在棧內存中保存的實際上是對象在堆內存中的引用地址, 通過這個引用地址可以快速查找到保存在堆內存中的對象。存放在堆內存中的對象,每個空間大小不一樣,要根據情況進行特定的配置。
如下代碼示例:
var a = 12; var b = false; var c = 'string' var obj = { name: 'sunshine' }
特點
棧區的特點:空間小,數據類型簡單,讀寫速度快,一般由JS引擎自動釋放
堆區的特點:空間大,數據類型復雜,讀寫速度稍遜,當對象不在被引用時,才會被周期性的回收。
瞭解瞭內存的棧區和堆區後, 接下來,來看看變量如何在棧區和堆區“愉快的玩耍”。
變量賦值
下面來看一組基本類型的變量傳遞的例子:
let a = 100 let b = a a = 200 console.log(b) // 100
初始棧中 a 的值為100;其次棧區中添加 b,並且將a復制瞭一份給b;最後 a保存瞭另外一個值 200,而b的值不會改變。
再來看一組引用類型傳遞的例子:
let obj1 = { name: 'a' } let obj2 = obj1 obj2.name = 'b' console.log(obj1.name) // b
以上代碼中,obj1 和 obj2 指向瞭同一個堆內存,obj1 賦值給 obj2,實際上這個堆內存對象在棧內存的引用地址復制瞭一份給瞭 obj2,所以 obj1 和 obj2 指針都指向堆內存中的同一個。
圖解如下:
綜合案例:
var a = [1, 2, 3, 4] var c = a[0] // 這時變量c是基本數據類型,存儲在棧內存中;改變棧中的數據不會影響堆中的數據 c = 5 console.log(c) // 5 console.log(a[0]) // 1 let b = a // b是引用數據類型,棧內存指針和 a一樣都指向同一個堆內存,改變數值後,會影響堆中的數據 b[2] = 6 console.log(a[2]) // 6
劃重點:在JS的變量傳遞中,本質上都可以看成是值傳遞,隻是這個值可能是基礎數據類型,也可能是一個引用地址,如果是引用地址,我們通常就說為引用傳遞。JS中比較特殊,不能直接操作對象的內存空間,必須通過指針(所謂的引用)來訪問。
所以,即使是所有復雜數據類型(對象)的賦值操作,本質上也是值傳遞。在往下看一下不同的值在參數中是如何傳遞的。
參數傳遞
由上可知,ECMAScript中所有函數的參數都是按值傳遞的。這意味著函數外的值會被復制到函數內部的參數中,就像從一個變量賦值到另一個變量一樣。在按值傳遞參數時,值會被復制到一個局部變量(arguments對象中的一個槽位)。在按引用傳遞參數時,值在內存中的位置會被保存在一個局部變量,這意味著對本地變量的修改會反映到函數外部。
下面看一個例子:在 bar 函數中,當參數為基本數據類型時,函數體內會賦值一份參數值,而不會影響原參數的實際值。
let foo = 1 const bar = value => { // var value = foo value = 2 console.log(value) } bar(foo) // 2 console.log(foo) // 1
如果將函數參改為引用類型,結果就不一樣瞭:
let foo = { bar: 1} const func = obj => { // var obj = foo obj.bar = 2 console.log(obj.bar) } func(foo) // 2 console.log(foo.bar) // 2
從以上代碼中可以看出,如果函數參數是一個引用類型的數據,那麼當在函數體內修改這個引用類型參數的某個屬性時,也將對原來的參數進行修改,因為此時函數體內的引用地址指向瞭原來的參數。
但是,如果在函數體內直接修改對參數的引用,則情況又會不一樣:
let foo = { bar: 1} const func = obj => { // var obj = 2 obj = 2 console.log(obj) } func(foo) // 2 console.log(foo) // { bar: 1 }
這是因為如果我們將一個已經賦值的變量重新賦值,那麼它將包含新的數據或引用地址。這時函數體內新創建瞭一個引用,任何操作都不會影響原參數的實際值。
如果一個對象沒有被任何變量指向,JavaScript引擎的垃圾回收機制會將該對象銷毀並釋放內存。
小結
- 函數參數為基本數據類型時,函數體內賦值瞭一份參數值,任何操作都不會影響原參數的實際值
- 函數參數是引用類型時,當函數體內修改這個值的某個屬性時,將會對原來的參數進行修改
- 函數參數是引用類型時,如果直接修改這個值的引用地址,則相當於在函數體內新創建瞭一個新的引用,任何操作都不會影響原參數的實際值。
面試題
- 參數多次賦值問題
function func (person) { person.age = 25 person = { age: 50 } return person } var person1 = { age: 30 } var person2 = func(person1); console.log(person1) console.log(person2)
答案:{ age: 25 },{ age: 50 }。因為函數內部,person 第一次修改,相當於 復制瞭 person1 的內存地址給person,第二次修改是創建一個新的 person 變量。所以 person1 在堆內存中的值會被修改,person 也是新的 person 變量返回的值
- 變量幹擾問題
let obj1 = { x: 100, y: 200} let obj2 = obj1 let x1 = obj1.x obj2.x = 101 x1 = 102 console.log(obj1)
答案:{ x: 101, y: 200 },x1是幹擾項,因為obj.x是原始類型值,所以修改後不會影響原數據的引用地址。
兩者的區別就是:
舉個例子:
值傳遞:A覺得B的房子裝修風格很好,於是借用瞭B的裝修風格。但是過瞭段時間A給房子裡面又添加瞭點別的風格,但是B的房子風格還是原來的。
引用傳遞:A喜歡B的房子風格,借用瞭人傢的風格,過瞭段時間A給傢裡添加瞭新的風格,但是A覺得自己的風格比B的好,於是通過B給A的地址,去B的傢硬是把人傢的風格改成和自己一樣的瞭。
總結
到此這篇關於JS中"值傳遞"和"引用傳遞"的文章就介紹到這瞭,更多相關JS 值傳遞和引用傳遞內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JavaScript的八種數據類型
- 一篇文章弄懂js中的typeof用法
- TypeScript保姆級基礎教程
- TypeScript中定義變量方式以及數據類型詳解
- JavaScript原始值與包裝對象的詳細介紹