輕鬆理解javascript繼承

輕鬆理解javascript繼承

原型繼承是javascript中繼承最傳統的做法,通過操作僞類和prototype,你很容易就能實現繼承的效果:將父類的實例賦給子類的prototype,再創建子類實例即可。

如果你不知道原型的基礎知識,我強烈推薦你閱讀我的上一篇文章《輕鬆理解javascript原型》,文章中,作者把原型部分的知識近乎完美的剝離了出來,目的是爲這篇文章做好鋪墊。

但實話實說,我和《javascript語言精粹》的作者“老道”有着近乎相同的想法——我們希望能夠避免new關鍵字的出現和僞類帶來的種種問題,而採取另一種產生對象的設計模式——讓函數直接返回對象。

這篇文章會爲大家詳細的介紹原型繼承的應用、問題和需要注意的地方,更會花上一定的篇幅爲大家推薦應用模塊差異化繼承。

僞類與原型繼承

一個例子

你知道,在javascript中,沒有純粹的類的概念,就像“老道”說的那樣:javascript只關心對象能做什麼,並不關心它是怎麼來的。

所謂原型繼承,其實就是利用js訪問對象屬性(或方法)的時候會去查找它的原型鏈這一特點:你把父類對象的實例賦值給子類構造函數的原型屬性,這樣子類的所有實例在創建時都會通過__proto__子類構造函數.prototype的隱祕連接“獲得”那個對象的所有屬性和方法——子類對象從父類對象繼承的屬性並不真正直接存在於子類對象上,而是存在於子類構造函數的prototype(原型)對象中或者說子類實例的__proto__屬性上,你可以在子類對象上通過原型鏈訪問到它們。如果你還不知道是原型鏈查找是怎麼運作的,《輕鬆理解javascript原型》裏講得很清楚。

下面的例子會更好地幫助你理解:

// 動物類
var Animal = function () {
    this.kind = "animal";
};

// 哺乳動物類
var Mammal = function (name) {
    this.name = name;
    this.sayHello = function () {
        alert('hello I am' + this.name + " and I am a kind of" + this.kind);
    }
};

Mammal.prototype = new Animal();

var mammal = new Mammal('Tom');
mammal.sayHello();

輸出:

hello I am Tom and I am a kind of animal

當Mammal的實例mammal調用sayHello()方法時,裏面需要找到this.kind,但是對象本身並沒有this.type,於是javascript開啓”大招”——原型鏈查找。去查找mammal構造函數Mammal的prototype對象,結果在這裏面發現了“贓物”屬性——kind,返回結果,遊戲結束。

弄丟了什麼

這麼做並不是完美無缺的,不知道你還記不記得我們曾經說過的關於構造器的事,我不介意讓這段代碼再次呈現在這裏:

this.prototype = {constuctor : this}

是不是想起什麼來了,(構造)函數的prototype並非一個任何意義上的空對象——在這個函數對象被創建的時候,它就被它的構造器Function強行刻下了烙印,一個名叫constructor的屬性,指的正是它自己。

如果用另一個實例去替換函數的prototype的話,顯然,對象會丟失原本的constuctor屬性,我們輸出一下mammal的constuctor:

function () {
    this.kind = "animal";
}

是Animal的構造函數,不難想象,Mammal的prototype被Animal的實例替換掉了,原本的constuctor屬性也隨之丟失了。而Animal構造器的prototype裏也有一個叫做construcor的屬性,值是上面輸出的結果。

所以,如果需要的話,爲了保留住真正的構造器,你可以在替換了Mammal的prototype之後再做些額外的工作:

Mammal.prototype.constructor = Mammal;

此時再訪問mammal的constructor屬性,會返回Mammal構造函數。

更新

當構造器的prototype對象更新時,之前產生的所有實例對象都會做出立即更新。我們把上面動物的例子稍作改變:

// 動物類
var Animal = function () {};

// 哺乳動物類
var Mammal = function (name) {
    this.name = name;
    this.sayHi = function () {
        console.log('hello I am ' + this.name + " and I am from " + this.country);
    }
};

Mammal.prototype = new Animal();

var mammal = new Mammal('Tom');

mammal.sayHi();

Mammal.prototype.country = 'USA'; // 顯然 Animal.prototype.country = 'USA'效果相同

mammal.sayHi();

先後輸出:

hello I am Tom and I am from undefined
hello I am Tom and I am from USA

但是,你要注意我說的是更新,如果你完全將prototype換成了別對象,那麼引用關係將被破壞,之前生成的實例對象將會不爲所動。

僞類的缺陷

僞類加原型的方式的的確確能夠解決對象與繼承的問題,但這種方式存在缺陷:

(1) 首先,它沒有私有環境,所有屬性都是公開的

function Cat() {
    this.name='Tom';
    this.age = 2;
}

var cat = new Cat();

打開控制檯,輸入cat.name,就可以輕而易舉的知道我們的小貓叫做Tom,在控制檯輸入cat.name = ‘Jerry’,糟糕,我們小貓的名字被修改了!!

(2)其次,它無法像java那樣通過super.屬性輕易的訪問父類名字相同的屬性和方法,除非使用cat.__proto__這樣不友好的方式,這是應該禁止的。(事實上,直接訪問屬性都是應該被禁止的)

function Animal () {
    this.name = "animal"
}

function Cat() {
    this.name='Tom';
    this.age = 2;
}

Cat.prototype = new Animal();
var cat = new Cat();

console.log(cat.name); // =>Tom
console.log(cat.__proto__.name); // =>animal

(3)最可怕的如果你在調用構造器函數時忘記了在前面加上new前綴,那麼this將不會被綁定到那個新對象上,反倒被綁定到全局對象上,從而破壞了全局環境。既沒有編譯時警告,也沒有運行時警告。

或許,我是說或許,是時候換一個設計模式以實現相同並更強大的功能。

應用模塊模式繼承

差異化繼承

差異化繼承的本質是先複製一份與父對象含有相同屬性特徵的對象(注意是深複製,不是簡單的複製一個引用),然後再在這個基礎上創建我們自己的對象:

var animal = {
    name : "animal",
    age : 15
};

var cat = Object.create(animal);

cat.name = "Tom";
cat.sayMeow = function () {
    console.log('meow')
};

console.log(cat.name);
console.log(cat.age);
cat.sayMeow();

Object.create是ES5的新特性,作用是創建一個具有指定原型且可選擇性地包含指定屬性的對象(我們不討論它的第二個作爲屬性描述符集合的可選參數)。在父對象的這個基礎上,我們再爲子對象添加新屬性。

在ES3的那個原始的時代,Object.create()大概是像下面這樣工作的:

function create (obj) {
    var F = function () {};
    F.prototype = obj;
    return new F();
}

實際上,父對象的屬性與僞類繼承模式類似,被放置在了原型上,而不是真正的存在與子對象上。

函數化

想想僞類模式的缺陷吧,訪問父元素的同名屬性的問題解決了(直接父對象.屬性就可以),遺漏new關鍵字造成的威脅解決了,但私有屬性仍然暴漏無疑。我們使用函數化的方式解決這個問題,先看一個簡單的例子:

function animal() {
    var name = 'animal';
    var getName = function () {
        return name
    };
    return {
        getName : getName
    }
}

var myAnimal = animal();
console.log(myAnimal.name);  // =>undefined
console.log(myAnimal.getName()); //=>animal

這裏我們利用閉包的特性來解決私有變量的問題。我在想或許我應該寫一篇叫做《輕鬆理解javascript閉包的文章》。

現在好了,除非調用我們的getName特權方法,誰也別想知道name是什麼,更別想修改它。看那,animal()已經不是new的小跟班,它自己返回一個對象,它的首字母再也不用大寫了。

應用模塊繼承

幹得不錯,現在,我們試着融合兩種方法,實現模塊化繼承。爲了更明顯些,我們爲animal構造器增加一個food屬性和eat方法。嗯,我已經盡力把情況做的豐富些:

function animal(info) {

    var that = {};

    that.getName = function () {
        var name = info.name|| 'animal';
        console.log('(super method) my name is ' + name);
        return name ;
    };

    that.eat = function () {
        console.log('I am eating ' + info.food);
    };

    return that;
}

function cat(info) {

    var that = animal(info), // 獲得父級對象,並在此基礎上創建子對象
        super_getName = that.getName; // 直接獲得父級對象的方法

    that.sayMeow = function () {    // 子對象特有的方法
        console.log('Meow')
    };

    that.super_getName = super_getName;

    that.getName = function () {  // 子對象與父對象重名的方法
        var name = info.name|| 'Tom';
        console.log('(child method) my name is ' + name);
        return name ;
    };

    return that;
}

var Tom = cat({name:'Tom', food:'fish'});

Tom.sayMeow(); // =>Meow
Tom.eat(); // =>I am eating fish
Tom.getName(); // =>(child method) my name is  Tom
Tom.super_getName();// =>(super method) my name is  Tom

“構造函數”接收一個參數info,存放對象的所有數據信息。這種設計模式下,私有變量受到保護,可以輕易的訪問父級對象的方法(需要時使用apply改變this),更解決了new關鍵字容易造成的問題。

小結

通過今天的學習,不知道你有沒有走出原型、繼承的謎團呢? 如果還是有疑惑,我推薦你再讀一遍。《javascript語言精粹》、《javascript啓示錄》中對相關的知識點有更細緻的講解,沒事的話就去看看這些書吧,的確挺好的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章