首頁 > 軟體

一篇文中告訴你JS中的"值傳遞"和"參照傳遞"

2022-05-14 19:01:46

前言

現代的前端開發,不再是刀耕火種的 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 值傳遞和參照傳遞內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com