JavaScript引數傳遞的深入理解

Macho發表於2017-09-17

今天看到《JavaScript高階程式設計》裡面關於引數傳遞的章節時,有點懵。本著“打破砂鍋問到底”的精神,看了些別人寫的部落格和知乎上一些大神的解釋,算是對引數傳遞有了個比較全面的瞭解。

變數在記憶體中的存放方式

在講引數傳遞前,先要理解變數在記憶體中的存放方式。ECMAScript變數有可能是5種基本型別的值(Undefined,Null,Boolean,Number,String),也有可能是引用型別值(Object),即物件。

對於值是5種基本型別的變數而言,變數是放在棧記憶體裡的。因為這些變數佔據的記憶體是固定的,這樣儲存便於迅速查尋變數的值。例如:

var name = "Nicholas",

    city = "Beijing",

   age = 22;

這些變數的儲存結構為:

JavaScript引數傳遞的深入理解

變數名和變數的值都放在棧記憶體。

而對於值是引用型別值的變數而言,是同時儲存在棧記憶體和堆記憶體中的。例如:

var obj1 = {name:"Nicholas"},

    obj2 = {name:"Greg"};

這些變數的儲存結構為:

JavaScript引數傳遞的深入理解

在棧記憶體裡沒有直接存物件,而是存的物件在堆記憶體中的地址。物件的屬性和方法都包含在物件裡。

複製變數值

瞭解了變數在記憶體中的儲存方式後,還要理解變數賦值的過程。用一個變數向另一個變數賦值時,基本型別值和引用型別值也會有所不同。如果用一個變數向新變數賦基本型別值,會在變數上建立一個新值,然後把該值賦給為新變數分配的位置上。例如:

var num1 = 5,

    num2 = num1,

    num1 = 10;

alert(num2); //5

當用變數num1為num2賦值時,num2中也儲存了值5。但這個5與變數num1中的5是相互獨立的,互不影響。即便後來num1的值變為10,num2的值還是5。

當從一個變數向另一個變數複製引用型別的值時,同樣也會把儲存在變數物件中值複製一份到新變數分配的空間中。前面提到過,這個值實際上是一個指標,而這個指標是指向儲存在堆中的一個物件。由於這兩個變數的值相同(即指標相同,指向同一個物件),所以改變一個變數的時,另一個變數也會改變。例如:

var obj1 = new Object(),

    obj2 = obj1;

   obj1.name = "Nicholas";

   alert(obj2.name);//"Nicholsa"

   obj2.name = "Greg";

   alert(obj1.name);//"Greg"

首先是變數obj1儲存了一個物件的新例項,當obj1的值賦給obj2時,實際上是把obj1指向這個物件的地址賦給了obj2,然後obj2也指向這個新物件。當為obj1新增屬性後,obj2也能訪問這個屬性,並且屬性值是相同的。因為這兩個變數指向的是同一個物件。

JavaScript引數傳遞的深入理解

但是如果為obj2賦值後,又新建一個物件例項賦值給obj2,那麼obj2將不在指向obj1。obj1和obj2將相互獨立,互不影響。例如:

var obj1 = new Object(),

    obj2 = obj1,

    obj2 = new Object();//新建一個物件例項,將在堆記憶體中重新分配地址空間

    obj1.name ="Nicholas";

    alert(obj2.name); //undefined

    obj2.name = "Greg";

    alert(obj1.name); //"Nicholas"

傳遞引數

講完前面兩點,可以進入正題了——JS中函式引數的傳遞方式。

函式引數傳遞的過程實際就是實參向形參複製值的過程。在向引數傳遞基本型別的值時,被傳遞(實參)的值會複製給一個區域性變數(形參),形參值的變化不會對函式外的實參產生影響。在向引數傳遞引用型別的值時,會把這個值在記憶體中的地址複製給形參。這時這個形參也指向了函式外的實參,因此這個形參的變化也會導致實參的變化。例如:

function addTen(num) { 

   num += 10;

   return num;

}

var  count = 20,

result = addTen(count);

alert(count);//20,形參值的變化不會影響實參

alert(result);//30

這裡函式addTen()的引數num,實際上是函式的區域性變數。在呼叫函式時,變數count作為引數傳遞給函式。由於count的值是20,所以數值20被複制給引數num。在函式內部,這個引數被加了10,但這並不會影響函式外部的count變數。

當向引數傳遞的值為物件時,例如:

function setName(obj) { 

obj.name = "Nicholas";

}

var person = new Object();

person.name = "Greg";

setName(person);

alert(person.name);//"Nicholas"

這裡首先是建立了一個物件,儲存在變數person中,並且給變數的name屬性賦值為"Greg"。然後這個變數被當作引數傳遞給函式setName的引數obj。在函式內部,obj和person指向同一個物件,因為傳遞的是物件的地址。所以給obj的name屬性賦值後,也會改變person的name屬性值。但如果在函式內部為obj新建一個物件例項,這個新物件例項會開闢新的記憶體空間,導致obj的地址和person不同。此時,obj和person將指向兩個不同的物件,所以互不影響。例如:

function setName(obj){

    obj.name = ""Nicholas;//這個obj和person指向的地址相同,即函式外person建立的物件。

    obj = new Object();//新建例項物件,導致obj指向另一個地址

    obj.name = "Greg";

}

var person = new Object();

person.name = "Jhon";

setName(person);

alert(person.name);//"Nicholas"

再舉個例子:

var obj1 = { value:'111'};

var obj2 = { value:'222'};

function changeStuff(obj){ 

    obj.value = '333'; 

    obj = obj2; 

    return obj.value;

}

var foo = changeStuff(obj1);

console.log(foo);// '222' 引數obj指向了新的物件

console.log(obj1.value);//'333'

整個過程可以用下圖表示:

JavaScript引數傳遞的深入理解


總結:JavaScript函式的引數都是按值傳遞的。基本型別值的傳遞是按值傳遞,引用型別值的傳遞也是按值傳遞,只不過這個值存放的是地址。


相關文章