今天看到《JavaScript高階程式設計》裡面關於引數傳遞的章節時,有點懵。本著“打破砂鍋問到底”的精神,看了些別人寫的部落格和知乎上一些大神的解釋,算是對引數傳遞有了個比較全面的瞭解。
變數在記憶體中的存放方式
在講引數傳遞前,先要理解變數在記憶體中的存放方式。ECMAScript變數有可能是5種基本型別的值(Undefined,Null,Boolean,Number,String),也有可能是引用型別值(Object),即物件。
對於值是5種基本型別的變數而言,變數是放在棧記憶體裡的。因為這些變數佔據的記憶體是固定的,這樣儲存便於迅速查尋變數的值。例如:
var name = "Nicholas",
city = "Beijing",
age = 22;
這些變數的儲存結構為:
而對於值是引用型別值的變數而言,是同時儲存在棧記憶體和堆記憶體中的。例如:
var obj1 = {name:"Nicholas"},
obj2 = {name:"Greg"};
在棧記憶體裡沒有直接存物件,而是存的物件在堆記憶體中的地址。物件的屬性和方法都包含在物件裡。
複製變數值
瞭解了變數在記憶體中的儲存方式後,還要理解變數賦值的過程。用一個變數向另一個變數賦值時,基本型別值和引用型別值也會有所不同。如果用一個變數向新變數賦基本型別值,會在變數上建立一個新值,然後把該值賦給為新變數分配的位置上。例如:
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也能訪問這個屬性,並且屬性值是相同的。因為這兩個變數指向的是同一個物件。
但是如果為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函式的引數都是按值傳遞的。基本型別值的傳遞是按值傳遞,引用型別值的傳遞也是按值傳遞,只不過這個值存放的是地址。