iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
0
自我挑戰組

邁向 JavaScript 核心之路 系列 第 20

[Day 20] JavaScript 物件繼承- 繼承原型方法

  • 分享至 

  • xImage
  •  

參數傳遞

參數傳遞的過程是將變數在記憶體的位置參考複製到 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?


上一篇
[Day 19] JavaScript 物件繼承- 繼承 ( Inherit )
下一篇
[Day 21] JavaScript 物件繼承- 傳遞參數到父層類別
系列文
邁向 JavaScript 核心之路 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言