從問題入手,深入瞭解JavaScript中原型與原型鏈

_Fatman發表於2021-02-06

從問題入手,深入瞭解JavaScript中原型與原型鏈

前言

開篇之前,我想提出3個問題:

  1. 新建一個不新增任何屬性的物件為何能呼叫toString方法?
  2. 如何讓擁有相同建構函式的不同物件都具備相同的行為?
  3. instanceof關鍵字判斷物件型別的依據是什麼?

要是這3個問題都能回答上來,那麼接下來的內容不看也罷。但若是對這些問題還存在疑慮和不解,相信我,下面的內容將正是你所需要的。

正文

新建一個不新增任何屬性的物件為何能呼叫toString方法?

我在深入瞭解JavaScript中基於原型(prototype)的繼承機制一文中提到過,JavaScript使用的是基於原型的繼承機制,它的引用型別與其對應的值將都存在著__proto__[1]屬性,指向繼承的原型物件[2]。當訪問物件屬性無果時,便會在其原型物件中繼續查詢,倘若其原型物件中還是查詢無果,那便接著去其原型物件的原型中去查詢,直到查詢成功或原型為null時[3]才會停止查詢。

let obj = {
}
obj.toString();//"[object Object]"

這段程式碼就是在obj物件中查詢toString方法,查詢無果,繼而在其原型[4]中查詢toString方法,正好其原型中含有toString方法,故而得以輸出"[object Object]"

如何讓擁有相同建構函式的不同物件都具備相同的行為?

下面是一段實現了釋出訂閱模式的程式碼:

let _indexOf = Array.prototype.indexOf;
let _push = Array.prototype.push;
let _slice = Array.prototype.slice;
let _concat = Array.prototype.concat;
let _forEach = Array.prototype.forEach;

function Publish(){
    this.subList;
    
    this.indexOf = function(sub){
        let index = -1;
        if(typeof this.subList === 'undefined' || this.subList === null){
            this.subList = [];
        }
        if(typeof sub !== 'undefined' && sub !== null){
            index = _indexOf.call(this.subList,sub);
        }
        return index;
    }

    this.addSub = function(sub){
        let index = this.indexOf(sub);
        index > -1 ?
            '' : 
            _push.call(this.subList,sub);
    };

    this.removeSub = function(sub){
        let index = this.indexOf(sub);
        index > -1 ?
            index === 0 ?  
                this.subList = _slice.call(this.subList,1) :
                this.subList = _concat.call(_slice.call(this.subList,0,index),_slice.call(this.subList,index + 1)) :  
            '';
    };

    this.notifySingle = function(sub,msg){
        let index = this.indexOf(sub);
        index > -1 ?
            (typeof sub.onReceive === 'function' ? 
                sub.onReceive(msg) : 
                '') : 
            '';
    };

    this.notifyAll = function(msg){
        if(typeof this.subList !== 'undefined' && this.subList !== null){
            _forEach.call(this.subList,(sub)=>{
                if(typeof sub !== 'undefined' && sub !== null){
                    typeof sub.onReceive === 'function' ? 
                        sub.onReceive(msg) : 
                        '';
                }
            })
        }
    };
}

function Subscription(name){
    this.name = name;
    this.onReceive = function(msg){
        console.log(this.name + ' 收到訊息 : ' + msg);
    };
}

let pub = new Publish();
let sub1 = new Subscription('sub1');
let sub2 = new Subscription('sub2');
let sub3 = new Subscription('sub3');
let sub4 = new Subscription('sub4');

pub.addSub(sub1);
pub.addSub(sub1);
pub.addSub(sub2);
pub.addSub(sub3);
pub.addSub(sub4);

pub.notifyAll('這是一條全部推送的訊息');
// sub1 收到訊息 : 這是一條全部推送的訊息
// sub2 收到訊息 : 這是一條全部推送的訊息
// sub3 收到訊息 : 這是一條全部推送的訊息
// sub4 收到訊息 : 這是一條全部推送的訊息

pub.notifySingle(sub2,"這是一條單獨推送的訊息");
// sub2 收到訊息 : 這是一條單獨推送的訊息

pub.removeSub(sub3);

pub.notifyAll('這是一條全部推送的訊息');
// sub1 收到訊息 : 這是一條全部推送的訊息
// sub2 收到訊息 : 這是一條全部推送的訊息
// sub4 收到訊息 : 這是一條全部推送的訊息

此程式碼中擁有同一建構函式的所有物件都含有不同的方法。

sub1.onReceive === sub2.onReceive;//false
sub1.onReceive === sub3.onReceive;//false
sub1.onReceive === sub4.onReceive;//false
sub2.onReceive === sub3.onReceive;//false
sub2.onReceive === sub4.onReceive;//false
sub3.onReceive === sub4.onReceive;//false

這樣會導致:
1.浪費記憶體;
2.不易於對方法進行批量操作。

接下來是改進版本,使用原型達到程式碼複用的效果

let _indexOf = Array.prototype.indexOf;
let _push = Array.prototype.push;
let _slice = Array.prototype.slice;
let _concat = Array.prototype.concat;
let _forEach = Array.prototype.forEach;

function Publish(){
    this.subList;
}

Publish.prototype.indexOf = function(sub){
    let index = -1;
    if(typeof this.subList === 'undefined' || this.subList === null){
        this.subList = [];
    }
    if(typeof sub !== 'undefined' && sub !== null){
        index = _indexOf.call(this.subList,sub);
    }
    return index;
}

Publish.prototype.addSub = function(sub){
    let index = this.indexOf(sub);
    index > -1 ?
        '' : 
        _push.call(this.subList,sub);
};

Publish.prototype.removeSub = function(sub){
    let index = this.indexOf(sub);
    index > -1 ?
        index === 0 ?  
            this.subList = _slice.call(this.subList,1) :
            this.subList = _concat.call(_slice.call(this.subList,0,index),_slice.call(this.subList,index + 1)) :  
        '';
};

Publish.prototype.notifySingle = function(sub,msg){
    let index = this.indexOf(sub);
    index > -1 ?
        (typeof sub.onReceive === 'function' ? 
            sub.onReceive(msg) : 
            '') : 
        '';
};

Publish.prototype.notifyAll = function(msg){
    if(typeof this.subList !== 'undefined' && this.subList !== null){
        _forEach.call(this.subList,(sub)=>{
            if(typeof sub !== 'undefined' && sub !== null){
                typeof sub.onReceive === 'function' ? 
                    sub.onReceive(msg) : 
                    '';
            }
        })
    }
};

function Subscription(name){
    this.name = name;
    
}

Subscription.prototype.onReceive = function(msg){
    console.log(this.name + ' 收到訊息 : ' + msg);
};

let pub = new Publish();
let sub1 = new Subscription('sub1');
let sub2 = new Subscription('sub2');
let sub3 = new Subscription('sub3');
let sub4 = new Subscription('sub4');

pub.addSub(sub1);
pub.addSub(sub1);
pub.addSub(sub2);
pub.addSub(sub3);
pub.addSub(sub4);

pub.notifyAll('這是一條全部推送的訊息');
// sub1 收到訊息 : 這是一條全部推送的訊息
// sub2 收到訊息 : 這是一條全部推送的訊息
// sub3 收到訊息 : 這是一條全部推送的訊息
// sub4 收到訊息 : 這是一條全部推送的訊息

pub.notifySingle(sub2,"這是一條單獨推送的訊息");
// sub2 收到訊息 : 這是一條單獨推送的訊息

pub.removeSub(sub3);

pub.notifyAll('這是一條全部推送的訊息');
// sub1 收到訊息 : 這是一條全部推送的訊息
// sub2 收到訊息 : 這是一條全部推送的訊息
// sub4 收到訊息 : 這是一條全部推送的訊息
sub1.onReceive === sub2.onReceive;//true
sub1.onReceive === sub3.onReceive;//true
sub1.onReceive === sub4.onReceive;//true
sub2.onReceive === sub3.onReceive;//true
sub2.onReceive === sub4.onReceive;//true
sub3.onReceive === sub4.onReceive;//true

改進版本與之前的版本相比有一個特點:擁有同一建構函式的物件,屬性是唯一的,行為是一致的[5]。所有物件都擁有獨立於其它物件的屬性,卻存在相同的行為。這正是因為在改進版本中,方法存在於建構函式的prototype屬性值上,其將被其建立的物件所繼承。也正是因為如此,儘管此時的sub1、sub2、sub3、sub4中都不包含onReceive方法,但也可以通過繼承的原型物件Subscription.prototype去達到呼叫onReceive的目的。而且修改Subscription.prototype上的onReceive方法是可以馬上作用到sub1、sub2、sub3、sub4上的將方法定義到建構函式的prototype屬性值上,就可以讓擁有相同建構函式的不同物件都具備相同的行為以達到程式碼複用目的

instanceof關鍵字判斷物件型別的依據是什麼?

我在深入瞭解JavaScript中基於原型(prototype)的繼承機制中宣告瞭函式Person,並以它為建構函式建立了person物件

function Person(){
	
}
let person = new Person();

person物件的繼承Person函式的prototype屬性值,而Person函式的prototype屬性值又繼承Object函式的prototype屬性值,這種一層一層繼承的關係構成了原型鏈。

instanceof關鍵字判斷物件型別的依據便是判斷函式的prototype屬性值是否存在於物件的原型鏈上。

正如Person函式的prototype屬性值和Object函式的prototype屬性值都存在於person物件的原型鏈上,所以使用instanceof判斷兩者都為true。

person instanceof Person;//true
person instanceof Object;//true

而Function函式的prototype屬性值不存在於person物件的原型鏈上,所以使用instanceof判斷Function函式為false。

person instanceof Function;//false

最後,完成一個instanceof。

/**
* obj 變數
* fn 建構函式
*/
function myInstanceof(obj,fn){
    let _prototype = Object.getPrototypeOf(obj);
    if(null === _prototype){
        return false;
    }
    let _constructor = _prototype.constructor;
    if(_constructor === fn){
        return true;
    }
    return myInstanceof(_prototype,fn);
}

//測試程式碼
myInstanceof({},Object);//true
myInstanceof([],Array);//true
myInstanceof(window,Window);//true
myInstanceof(new Map(),Map);//true
myInstanceof({},Array);//false
myInstanceof({},Function);//false

大功告成。

結尾

這3個問題的解答分別對原型和原型鏈的含義以及它們在JavaScript中起到了什麼作用進行了闡述。不過由於本人才疏學淺,難免會遇到一些我個人理解亦或是表達存在錯誤的地方,還望各位遇到之時,能不吝指出。


  1. 雖然__proto__已經被不推薦使用,但是為了更直觀,我在此文中獲取物件原型的方法都將通過物件的__proto__屬性,還望悉知。 ↩︎

  2. Object.prototype繼承的原型指向null。 ↩︎

  3. Object.prototype的原型為null,它是原型鏈的頂點,查到Object.prototype的原型時還找不到便會報找不到了。 ↩︎

  4. 物件obj的原型為obj的建構函式的prototype屬性,也就是Object.prototype。 ↩︎

  5. 這裡的屬性意指除方法外的屬性,行為意指方法。 ↩︎

相關文章