參數傳遞的過程是將變數在記憶體的位置參考複製到 function 的參數上,當修改這個參數的內容,實際上是去修改參考上的內容,若重新賦予新值則會建立新的記憶體參考,此稱共享參考 ( call by sharing ),但由於共享參考在運作上看起來很像傳值參考 ( call by value ) 與傳址參考 ( call by referencd ),差別在於原始型別的值是不可變動的 ( immutable )。
而 JavaScript 實做的是共享參考,但執行起來卻很像傳值參考、傳址參考。
為什麼會有這樣的錯覺呢?由於原始型別的值是不可變動的 ( immutable ),所以無法修改,永遠都只會賦予新值,所以看起來很像傳值參考。
而物件型別雖然是可變動的 ( mutable )的,但常常忽略掉傳址參考 ( call by referencd )賦予新值會改變原有的參照這項原則,所以很容易被誤解成是傳址參考。
new 這個動作其實就是跟記憶體要求一個空間來存放新建立的物件,也同等於 Objcet.create([prototype]) 和 [Class].call(obj) 這樣的操作。
若要修改物件的 prototype 則可以使用 Object.setPrototypeOf 這個方法。
關於 Objcet.create 繼承方法這部分,我們後續章節還會再提到。
回到主題,以下我們就用一段程式碼來驗證共享參考這概念。
// 共享參考
var arr1 = [1, 2, 3];
var arr2 = arr1;
arr2.push(4);
console.log( arr1 ); // [1, 2, 3, 4];
console.log( arr2 ); // [1, 2, 3, 4];
那回到昨天文章最後的問題,為什麼 call 能夠繼承屬性與方法呢?
因為透過 call 繼承時是藉由共享參考把 this 傳入父層類別。
不知道各位讀者有沒有發現,call 雖然可以繼承屬性與方法,但是他並無法繼承原型方法,因為原型方法是從 prototype 設定的,子層物件並非父層類別的原型,所以沒辦法透過 call 來繼承原型方法。
我們下面就來用一段程式碼來驗證 call 沒辦法繼承原型方法。
function A() {
this.abc = 12;
}
A.prototype.show = function() {
console.log( this.abc );
};
function B() {
A.call(this);
}
var obj = new B();
console.log(obj.abc); // 12
obj.show(); // obj.show is not a function
有興趣的讀者可以將這段程式碼放到 console 執行看看,
那話說回來,我們該如何解決無法繼承原型方法這個問題呢?
其實非常簡單,只要加上 B.prototype = A.prototype
這一句即可。
function A() {
this.abc = 12;
}
A.prototype.show = function() {
console.log( this.abc );
};
function B() {
A.call(this);
}
B.prototype = A.prototype;
var obj = new B();
console.log(obj.abc); // 12
obj.show(); // 12
雖然我們可以透過 prototype 來設定原型方法,但事情沒這麼間單,別忘記上述提到的共享參考這件事件。
function A() {
this.abc = 12;
}
A.prototype.show = function() {
console.log(this.abc);
}
function B() {
A.call(this);
}
B.prototype = A.prototype;
var objA = new A();
var objB = new B();
B.prototype.square = function() {
console.log( this.abc * this.abc );
};
objB.square(); // 144
objA.square(); // 144
以上這段程式碼驗證了透過 prototype 來設定原型方法,會影響到原有的父層類別的 prototype。
總歸問題還是回到了,我們該怎麼避免繼承原型方法時共享傳遞呢?
// prototype 設定一個物件
function A() {
this.abc = 12;
}
A.prototype.show = function() {
console.log(this.abc);
}
function B() {
A.call(this);
}
B.prototype = new A();
var objA = new A();
var objB = new B();
B.prototype.square = function() {
console.log( this.abc * this.abc );
};
objB.square(); // 144
objA.square(); // objA.square is not a function
我們透過 prototype 設定一個物件,如同我們先前原型鏈所述,當找不到方法或屬性時才會去原型裡面尋找。所以擴充子層類別的原型方法,就不會影響到父層類別囉!
不知不覺,鐵人賽已經進行到了三分之二,似乎寫文章已經默默變成一種習慣了?
參考資料:
Tommy - 深入 JavaScript 核心課程
TechBridge 技術共筆部落格 - 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?