獨家解析Javascript原型繼承

傳統面向對象的繼承和多態

咱們知道C++/Java/C#等面嚮對象語言,都原生地支持類的繼承。繼承的核心做用大抵是建立一個派生類,並使其複用基本類(即父類)的字段和/或方法。而且派生類能夠重寫基本類的方法。這樣基本類和派生類相同簽名的方法在被調用時,就會有不一樣的行爲表現,即爲多態的實質。換句話說,多態是透過繼承重寫實現的。javascript

舉例:實現不一樣的動物叫聲不一樣。
過程式編程(Java代碼):java

void animalSpeak(String animal) {
    if(animal == "Dog") {
        System.out.println("汪汪");
    } else if(animal == "Cat") {
        System.out.println("喵喵");
    } else {
      System.out.println("動物發聲");
    }
}

//調用代碼
animalSpeak("Dog"); //汪汪
animalSpeak("Cat"); //喵喵
animalSpeak("");    //動物發聲

這裏一個問題是,若是增長一種動物,就要在speak方法增長if分支,此方法逐漸變得臃腫難維護。偶爾因增長一種動物,會偶爾誤傷其餘動物的邏輯,也何嘗可知。面向對象式的動態應運而生。程序員

面向對象實現(java代碼)編程

class Animal {
    void speak() {
       System.out.println("動物發聲:");
    }
}

class Dog extends Animal {
    void speak() {
       super.speak();
       System.out.println("汪汪");
    }
}

class Cat extends Animal {
    void speak() {
       super.speak();
       System.out.println("喵喵");
    }
}

void animalSpeak(Animal animal) {
    animal.speak();
}

//調用代碼
animalSpeak(new Cat());     //動物發聲: 
                            //喵喵
animalSpeak(new Dog());     //動物發聲: 
                            //汪汪

當要增長一種動物時,只需增長一個class繼承 Animal,不會影響其餘已有的動物speak邏輯。可看出,面向對象多態編程的一個核心思想是便於擴展維護函數

結語:面向對象編程以繼承產生派生類和重寫派生類的方法,實現多態編程。核心思想是便於擴展和維護代碼,也避免if-elsethis

JavaScript繼承實現

Java繼承是class的繼承,而JavaScript的繼承通常是經過原型(prototype)實現。prototype的本質是一個Object實例,它是在同一類型的多個實例之間共享的,它裏面包含的是須要共享的方法(也能夠有字段)。
JavaScript版原型繼承的實現:prototype

function Animal() {
}
Animal.prototype.speak = function () {
    console.log('動物發聲:');
}

function Dog(name) {
    this.name = name;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
    //經過原型鏈找‘基本類’原型裏的同名方法
    this.__proto__.__proto__.speak.call(this);
    console.log('汪汪, 我是', this.name);
}

function Cat(name) {
    this.name = name;
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.speak = function () {
    //經過原型鏈找‘基本類’原型裏的同名方法
    this.__proto__.__proto__.speak.call(this);
    console.log('喵喵, 我是', this.name);
}

//調用代碼
function animalSpeak(animal) {
    animal.speak();
}

animalSpeak(new Dog('大黃'))
console.log()
animalSpeak(new Cat('小喵'))

//動物發聲:
//汪汪, 我是 大黃

//動物發聲:
//喵喵, 我是 小喵

JavaScript原型剖析

傳統面嚮對象語言的class繼承是爲代碼(方法和字段)複用,而JavaScript的prototype是在同類型實例之間共享的對象,它包含共享的方法(也可有字段)。因此java的class繼承和javascript的原型繼承,可謂異曲同工。爲了方便理解js原型,提出兩個概念:原型的design-time和run-time設計

Design-time 原型

可理解爲咱們(程序員)如何設計js類型的自上而下的繼承關係。以上例看出,design-time是經過prototype賦值實現。code

// Dog類型繼承自Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Run-time 原型和原型鏈

Run-time可理解爲,原型設計好以後,要建立實例了, 而且還能向上查找其繼承自哪一個原型對象

// 建立Dog類型的一個實例
var dog = new Dog('大黃');

// ***打起精神*** 這裏到了關鍵的地方:如何查找dog建立自哪一個類型:
// 顯而易見, dog是由Dog創造出來的
dog.__proto__ // Dog { constructor: [Function: Dog], speak: [Function] }

// 再往上查找原型鏈:
// 看出來是繼承了Animal的原型
dog.__proto__.__proto__  //Animal { speak: [Function] }

// 再往上查找原型鏈:
// 看出來是繼承了Object的原型
dog.__proto__.__proto__.__proto__ // {}

// 再往上查找原型鏈:
// 到了原型鏈的頂端。
dog.__proto__.__proto__.__proto__.__proto__ // null

因此在調用實例的方法,它會在原型鏈上自下而上,直到找到該方法。若是到了原型鏈頂端尚未找到,就拋錯了。

結語: design-time原型是經過prototype賦值,設計好自上而下的繼承關係; run-time時經過實例的__proto__,自下而上在原型鏈中查找須要的方法。

再論prototype 與__proto__

如前文述prototype可當作design-time的概念,以此prototype創造出一個實例。

var dog = new Dog('大黃');
//實質是:
//new 是便利構造方法
var dog = Object.create(Dog.prototype);
dog.name = '大黃'

__proto__是屬於實例的,可反查實例出自哪一個prototype。 因此dog.__proto__顯然等於Dog.prototype.

dog.__proto__ == Dog.prototype //true

而Dog.prototype創自於 Animal.prototype:

Dog.prototype = Object.create(Animal.prototype);

因此Dog.prototype的__proto__即爲Animal.prototype

Dog.prototype.__proto__ == Animal.prototype //true
 dog.__proto__.__proto__ == Animal.prototype //true

這樣就實現了run-time原型鏈自下而上的查找

結束語

原型繼承是JS老生常談的話題,也是很重要但不易深刻理解的技術點。本文裏提出了design-time原型設計和run-time原型鏈查找,但願有助於此技術點的理解。

後記

上文是對自定義函數類型實例的原型分析,漏掉了對JS內置類型的原型分析。你們知道JS內置類型是有:

undefined
null
bool
number
string
object
    Function
    Date
    Error
 symbol (ES6)

JS基本(primitive)類型原型分析

基本類型有undefined, null, bool, number, string和symbol. 基本類型是以字面量賦值函數式賦值的,而非經過new或Object.create(...)出來。
基本類型是沒有design-time的prototype。但undefined/null以外的基本練習,仍是有run-time的__proto__

// 經常使用基本類型的字面量賦值:
var b = true;
var n = 1;
var s = 'str';

// 經常使用基本類型的run-time __proto__
// 須要說明的是,基本類型自己是沒有任何方法和字段的。
// 例如undefined/null, 不能調用其任何方法和字段。
// 這裏調用b.__proto__時,會臨時生成一個基本類型包裝類的實例,
// 即生成var tmp = new Boolean(b)。這是個object實例,返回__proto__
b.__proto__ // [Boolean: false]
n.__proto__ // [Number: 0]
s.__proto__ // [String: '']
// 以上 *.__proto__ 打印出的是,字面量值出自哪一個函數, 因此亦能夠函數方式賦值, 跟字面量徹底等價。

// 基本類型的函數式賦值:
// *注意*的是,這裏僅是調用函數,沒有new。若用了new, 就構造出一個對象實例,而再非基本類型了
b = Boolean(true)
n = Number(1)
s = String('')
sym = Symbol('IBM') // ES6 新增的symbol沒有字面量賦值方式

// 特殊的case是undefined和null, 只有字面量賦值(沒函數方式賦值)
// null 和 undefined是沒有__proto__的。
// 也能夠理解爲,null處於任何原型鏈的最頂端,這是由於null是object類型(typeof null == 'object')。undefined不是object類型。
var nn = null;
var un = undefined;

JS基本類型的對象實例的原型分析

若是經過new 構造基本類型的對象實例,那麼就是對象而非原生態基本類型了。

var b = new Boolean(true)
var n = new Number(1)
var s = new String('')

它們就遵循自定義函數類型對象的原型法則了。以上述Number n爲例:

// Number的 design-time的prototype:
Number.prototype //[Number: 0]
typeof Number.prototype //'object'
Number.prototype instanceof Object // true. 原型自己就是一對象實例

// n的run-time __proto__原型鏈
n.__proto__ //[Number: 0],n是由Number函數構造產生的
// 可看出,n 繼承了object類型
n.__proto__.__proto__ // {}
// n的原型鏈頂端也是null
n.__proto__.__proto__.__proto__ // null

JS內置object類型的原型分析

JS內置的Date, Error, Function其自己就是function,就是說 typeof Date, typeof Error, typeof Function 都是 ‘function'. 因此讀者可用本文分析自定義函數類型原型的方法,自行分析這三者的design-time的prototype, 以及run-time的__proto__原型鏈。

須要特殊指出的是,咱們幾乎不會用new Function的方式去建立Function的實例,而是透過function關鍵字去定義函數。

// 通常是用function這個關鍵字,去定義函數。這和經過new Function構造本質是同樣的
function foo() { }

// 經過run-time的 __proto__,其實可看出,foo就是Function這個類型的一個實例
foo.__proto__ //[Function]
foo instanceof Foo // true

// 因此foo也就繼承了Function的design-time prototype
// 而理解這一點很重要。
add.__proto__ == Function.prototype // true

// 函數實例自己也是object類型
foo.__proto__.__proto__ //{}
foo instanceof Object // true

{}和Object.create(null)的區別

如下定義是等價的

var obj = {} // 字面量
var obj = new Object() // Object函數構造
var obj = Object.create(Object.prototype) // Object原型構造

obj run-time的__proto__即Object.prototype, 故obj繼承了Object.prototype的共享方法,例如toString(), valueOf()

obj.__proto__ == Object.prototype //true
obj.toString()                   //'[object Object]'
var obj2 = Object.create(null)
obj2.__proto__  // undefined
obj2.toString() //TypeError: obj2.toString is not a function

能夠看出,obj2無run-time的__proto__,沒有繼承Object.prototype,故而就不能調用.toString()方法了

相關文章
相關標籤/搜索