我們都知道在 ECMAScript 中,數據類型分為原始類型(又稱值類型/基本類型)和引用類型(又稱對象類型);這裡我將按照這兩種類型分別對函數進行傳參,看一下到底發生了什麼。 參數的理解 首先,我們要對函數的參數有一個瞭解: 形參就是函數內部定義的局部變數; 實參向形參傳遞值的時候,就是一個賦值操 ...
我們都知道在 ECMAScript 中,數據類型分為原始類型(又稱值類型/基本類型)和引用類型(又稱對象類型);這裡我將按照這兩種類型分別對函數進行傳參,看一下到底發生了什麼。
參數的理解
首先,我們要對函數的參數有一個瞭解:
形參就是函數內部定義的局部變數;
實參向形參傳遞值的時候,就是一個賦值操作,把實參的值直接複製一份給形參。
原始類型參數傳遞
示例1
var a = 1;
function f(b) {
a = 3;
}
f(a);
console.info(a); // 3
示例1中的代碼比較簡單,解析如下:
- 首先,我們定義了一個變數
a
,給它賦值為1
;又定義了一個函數f
,函數f
的形參是b
(此時,相當於在函數f
里定義了一個變數var b
; 它的值現在是undefined
); - 調用函數
f(a)
把a
作為實參傳入。這裡可以理解為,給b
進行了一次賦值操作b = 1
; - 接下來繼續執行代碼
a = 3
;註意:在函數體裡邊,出現了一個變數a
,但是它沒有用var
關鍵字定義,所以,它是一個全局作用域的變數,不是這個函數的局部變數,而在一開始,我們就定義了這樣一個變數a
,所以在這裡就是對之前的a
進行的又一次賦值操作,把a
從之前的1
變成了3
; - 執行完
f(a)
之後,把全局變數a
的值改變了,所以,當我們輸出a
查看它的值時,就得到了3
,而對於函數的形參b
根本沒有進行任何操作而已。
示例2
var a = 1;
function f(a) {
a = 3;
}
f(a);
console.info(a); // 1
解析:
示例2與示例1的區別,從錶面上看,就是形參 b
變成了 a
。但是,這樣的變化結果就是,對於函數 f
來說,參數起到了作用,當我們對函數 f
進行傳參操作的時候,我們傳入的實參在函數內部就會得到引用。
相比較示例1的第一步,函數 f
內部定義了一個變數 a
它的值是 undefined
,註意:在未執行函數的時候,只是進行了預解析,代碼沒有執行,在調用函數的時候才會開始執行代碼。
執行 f(a)
後,就是傳入實參 1
,函數內部的變數 a
賦值為 1
,然後再進行 a = 3
的操作 ,此時,在函數的局部作用域的棧記憶體中有一個變數 a
它的值是 3
;而在全局作用域的棧記憶體中,也存在一個變數 a
,它的值是 1
,這兩個變數是兩個不同的變數,只是它們的名字都是 a
而已。
過程如圖:
示例3
var a = 1;
function f(b) {
a = 3;
b = 10;
}
f(a);
console.info(a); // 3
console.info(b); // 報錯
解析:
下麵我們再來看一下示例3,先看變數 a
,完全和示例1一樣,這裡就不再詳細說明瞭;再看變數 b
,其實也是和示例2沒任何不一樣的地方的,只是在這裡把形參的名字改變了一下而已,在最後我們輸出 a
的時候,沒有任何問題,結果是 3
,但是,訪問變數 b
的時候,輸出會報錯,這裡涉及到的就是作用域問題了:在函數內部定義的變數,在函數內部可以訪問,而在函數的外部是無法訪問的。
具體過程可看下圖:
示例4
var a = 1;
function f(a) {
a = 3;
b = 10;
}
f(a);
console.info(a); // 1
console.info(b); // 10
解析:
綜合前三個示例,示例4我想大家都應該沒有什麼問題了,我們直接來看圖吧:
引用類型參數傳遞
下麵我們再來兩個引用類型參數的示例;以下示例僅為說明引用類型傳參之後,函數內部的賦值變化,所以用的都是簡單數組進行說明。
其實,引用類型參數的傳遞需要考慮的就是引用類型和原始類型之間的區別:
- 引用類型在預解析時候,和原始類型一樣都會在棧區里分配到空間,生成變數;但是賦值的時候,原始類型的賦值依然保存在棧記憶體當中,而引用類型就會在堆記憶體里占用一定空間存放它的數據,而在棧區里生成一個指向堆區的地址。
- 對變數進行複製的時候,原始類型的複製是在棧記憶體當中生成一份一樣的數據,可以理解為“完全複製”;而引用類型的複製,只是在棧記憶體中進行複製,兩個變數的地址同時指向堆記憶體中的那一份數據。
示例5
var a = [1];
function f(a){
a[100] = 3;
}
f(a);
console.info(a); // [1,100:3]
解析:
第一步:全局棧區中生成變數 a
,賦值後,在全局堆區里生成數組 [1]
;
第二步:傳參,調用 f(a)
,首先在函數棧區中生成變數 a
,此時值為 undefined
,然後把全局變數 a
的值賦給局部函數里的變數 a
,因為是引用類型的數組,所以,函數里的 a
在棧區里也生成一個指向全局堆里的相同 地址1
;
第三步:執行函數里的代碼 a[100] = 3
,找到函數棧中的 a
,再對它進行賦值,改變了全局堆中的 [1]
,得到了一個新的數組 [1,100:3]
;
第四步:訪問全局作用域中的變數 a
,得到指向堆記憶體中的數組 [1,100:3]
。
示例6
var a = [1];
function f(b){
b[100] = 3;
b = [1,2,3];
console.info(b); // [1,2,3]
}
f(a);
console.info(a); // [1,100:3]
解析:
該示例的關鍵點在第四步,執行到代碼 b = [1,2,3]
的時候,會對函數裡面的變數 b
重新進行一次賦值,這樣會在函數的堆記憶體中生成一個新的 數組,全局的 a
和函數中的 b
的指向地址就不一樣了,所以,它倆的輸出就不一樣了。
總結
在 js 中,原始類型是按值傳遞的;引用類型是按共用傳遞的。
按值傳遞 - call by value
一個外部變數傳遞給一個函數時,函數實參獲取到的實際上是這個外部變數的副本,在函數內部我們對實參進行的修改並不會反應到這個外部變數上。
按引用傳遞 - call by reference
一個外部變數傳遞給一個函數時,函數實參實際上是這個外部變數的一個引用,我們在函數內部對這個實參的任何修改,都會反應到這個外部變數。