1. 程式人生 > >淺談JavaScript原型鏈繼承方式與閉包

淺談JavaScript原型鏈繼承方式與閉包

JavaScript中的繼承方式有兩種,一種是通過call或apply方法借用建構函式,另一種方法則是通過原型來繼承。這篇文章用於梳理自己關於prototype繼承的知識點,也略微涉及一點點閉包的概念。

關於原型

在JavaScript中每當我們建立一個Function類的新物件,這個物件都會自動同時獲得一個名為prototype的屬性,這個屬性指向原型物件,它包含一個名為constructor的屬性,這個屬性始終指向建立當前物件的函式(建構函式)。

var obj = function(){
    this.attr= 'attr';
    this.method= function
() {
return 'method'; } }; console.log(obj.prototype); //Object {}

但是已經例項化的物件中是不存在prototype屬性的:

var obj1 = new obj();
console.log(obj1.prototype);
//undefined

那麼prototype去了哪裡呢?別急,回到我們開頭的程式碼片,我們去給prototype做個標記:

obj.prototype.property1 = 1;
console.log(obj.prototype);
//Object {property1: 1}

這裡可以看到原型物件中已經有了我們剛才定義的屬性property1: 1。
現在,我們例項化一個obj物件,然後列印它,嘗試把我們做的標記找出來:

var obj2 = new obj();
console.log(obj2);

是不是翻遍了所有屬性也沒有找到,只剩下最後一個看起來很奇怪的東西?

__proto__

展開它就會發現,我們為obj.prototype新增的屬性就在裡面。

這裡要引入一個新的概念,我覺得應該稱之為……物件的“實際原型”。將任意一個物件列印到控制檯,展開它的屬性列表,我們總是能看到這麼一個長得很奇怪的東西,我們為我們構造的物件——此處我認為應當稱之為“型別物件”——的prototype新增的屬性和方法,都在這裡面。

實際上這個東西在我們上面列印過的obj.prototype中也出現了,展開之後會發現在這裡面它本身沒有出現:因為實際上在這裡obj.prototype中的__proto__所指向的就是Javascript原型鏈的盡頭:Object.prototype

原型繼承

根據前文的結論,我們可以很簡單地做出推斷:當構造一個新物件的時候,如果將物件2的原型(prototype屬性)賦值為物件1的例項,那麼物件1的任何新例項都將擁有物件1所有的屬性和方法。
這相當於每當物件2被例項化,就建立一個物件1的只讀副本,對例項化的物件2做出的任何修改不會影響到原本的物件1,而對原本的物件2進行修改則會影響到每一個例項化的物件2,除非該物件2的同名屬性已經被修改和覆蓋。
此時我傾向於將型別物件1稱之為“父類物件”,型別物件2稱之為“子類物件”。

初學者有可能會認為這樣就實現了prototype繼承,實際上這樣也確實可以某種程度上達到目的:

var obj1 = function(){
    this.attr1 = 'a';
    this.method1 = function() {
        return 'this is method1';
    }
}
var obj2 = function(b){
    this.attr2 = b;
    this.method2 = function() {
        return 'this is method2';
    }
}
obj2.prototype = new obj1();

var obj = new obj2('a');
console.log(obj.method1());
//this is method1

閉包

但是,僅僅這樣還不夠。來看下面的程式碼片:

var obj1 = function() {
    this.attr1 = ['b','c'];
}
var obj2 = function (b) {
    this.attr2 = b;
}
obj2.prototype = new obj1();

var a1 = new obj2('d');
a1.attr1.push('e');
1.attr1 = 'b';
onsole.log(a1.attr1);
//'b'
ar a2 = new obj2('c');
console.log(a2.attr1);
//['b','c','e']

可以看到,雖然修改例項化的子類物件無法重寫原型物件obj1的屬性attr1,但卻可以使用push方法對obj2.prototype.attr1進行修改。
這是由於在JavaScript中,同一作用域中的引用型別複製操作,其複製的結果只是複製了對同一個物件的引用。實際上進行修改時,並不是修改了我們以為的物件的新副本,而是修改了被引用的物件,所有對其的引用都會受到影響。
在本例中,a1和a2的實際原型都是obj1的同一個例項,雖然作為原型被繼承的例項本身並不會由於子類物件的例項被修改而受到影響(重寫a1.attr1並不會影響到obj2.prototype.attr1),但是我們卻可以訪問並修改Array物件['b', 'c'],從而間接地改變了obj2.prototype的屬性。
在實際的應用中這是十分危險的,原型物件的屬性被修改往往會帶來一連串的問題。
解決這一問題的辦法就是將物件進行再次封裝:

//var attr1 = ['b', 'c'];
var obj2 = function () {
    var _attr1 = ['b', 'c'];
    var i = 0;
    var obj1 = function () {
        i++;
        this.attr1 = _attr1;
        //this.attr1 = attr1;
        this.attr3 = this.attr1;
        this.method1 = function () {
            document.write('attr3: ' + this.attr3 + '<br />');
        }
    }
    var OBJ2 = function (b) {
        this.a = i;
        this.attr2 = b;
    }
    OBJ2.prototype = new obj1();
    OBJ2.prototype.id = i;
    return new OBJ2();
}
var a1 = new obj2('d');
a1.attr1.push('f');
var a2 = new obj2('c');
console.log(a1.attr1);
//["c", "d", "f"]
console.log(a2.attr1);
//["c", "d"]
console.log(a1.attr1 === a2.attr1);
//FALSE

這樣通過將obj1和OBJ2的建構函式放在一個函式內,將它們的賦值操作限制在函式obj2的作用域內,就保護了我們不希望被修改的obj1的私有屬性。
為了保護私有屬性或者不汙染全域性環境而將變數等放置在一個函式中,使他們處於函式作用域中,這個方法有個專有名詞,就叫做閉包。

寫一篇部落格梳理一下,才發現自己對知識點的理解其實並沒有自己以為的那樣到位,歡迎各位交流、指教。