繼承與原型

咱們在討(mian)論(shi)JavaScript這門語言時,老是繞不過的一個話題就是「繼承與原型鏈」。那麼「繼承與原型鏈」究竟是什麼呢?segmentfault

我很喜歡的一個聊天模式是:我不能說XX是什麼,我只能說XX像什麼。也就是說我不直接跟你說定義,由於一般而言,「定義」所描述的概念很晦澀,好比關於「閉包」的定義——閉包是函數和聲明該函數的詞法環境的組合。閉包

因此,咱們先來看一下,JavaScript裏到底「繼承與原型鏈」是如何表現的。函數

「繼承與原型鏈」像什麼

不一樣於Java等的靜態語言,在JavaScript這門語言裏,咱們沒有「類」這個概念,全部的繼承都是基於原型的。咱們先直接看個例子:this

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_1 = {} // 咱們指望cat也能有sound屬性跟speak方法

obj_1.__proto__ = obj

console.log(obj_1.a) // 0
console.log(obj_1.f()) // 1

如上,咱們定義obj_1這個對象的時候,並無聲明a屬性跟f方法,可是咱們依然能夠找到它們。這是由於在JavaScript中,你在一個對象上尋找某個屬性(JavaScript對象都是鍵值對的形式,因此方法其實也能夠算一個屬性),他首先會在該對象本地尋找,若是沒有,他會順着原型鏈一層層往上尋找。prototype

在上面的栗子中,對象obj_1本地沒有定義任何屬性,因此當咱們執行obj_1.a的時候,會順着原型鏈往上找。在obj_1.__proto__ = obj這句裏,咱們將obj賦值給了obj_1__proto__屬性。插件

可是等等,__proto__是什麼?code

__proto__屬性指向的就是obj_1的原型,obj的原型是什麼呢?咱們能夠打印obj.__proto__來看看,結果打印出來一大堆東西,這些其實就是Object.prototype,也就是「終極原型」,這個對象再也不繼承任何原型。按照以前說的,obj_1應該也能直接訪問到這上面的屬性。事實也的確如此,好比:對象

obj_1.hasOwnProperty('a') // false

咱們並無在obj_1上定義hasOwnProperty方法,可是依然能夠找到該方法。事實上,全部以對象字面量(Object Literal)形式建立出來的對象,都繼承了有Object.prototype上的全部屬性。繼承

那麼咱們能不能建立一個不繼承自任何原型的對象呢?答案是能夠的。ip

JavaScript爲咱們提供了一個方法叫Object.create,經過它,咱們能夠建立一個原型爲特定對象的對象。若是咱們傳入一個null,那麼咱們就能建立一個「原型爲空」的對象。

var a = Object.create(null)

在這個例子裏,a成了一個空的對象,不只本地沒有任何屬性,連原型鏈都沒有,也就是說它甚至都沒有繼承Object.prototype。(思考:這樣的空對象到底有什麼做用呢?)

這樣一來,咱們也能夠利用Object.create來實現繼承咯?對的。

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_2 = Object.create(obj)
console.log(obj_2.a) // 0
console.log(obj_2.f()) // 1

可是從新想象,繼承的本質是什麼?繼承原型!那麼無論用什麼方法,只要在個人原型鏈上能找到你就好了。

如今有一個問題,obj上定義了一個屬性a,若是我在obj_2上再定義一個屬性a,那麼打印出來的會是誰的a呢?

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_2 = Object.create(obj)
obj_2.a = 2
console.log(obj_2.a) // 2

答案是顯而易見的,由於咱們在尋找一個屬性的時候,老是從當前對象本地開始的,若是在當前對象上找到了這個屬性,那麼查詢就中止了。因此,若是原型鏈過長,在查找一個靠前的原型上的屬性的時候,就會比較耗時。咱們應當儘可能避免這種過長的原型鏈。

「繼承與原型鏈」是什麼

讀到這裏,相信咱們已經可以對繼承原型鏈作一個定義了。

原型鏈

原型鏈就是從一個對象的__proto__開始,一直到這條線的最末端,大部分狀況下,這個最末端就是Object.prototype。例如上面的那個例子:

var obj = {
    a: 0,
    f: function() {
        return this.a + 1
    }
}

var obj_2 = Object.create(obj)

// obj_2.__proto__ === obj
// obj.__proto__ === Object.prototype

繼承

在這個例子裏,obj --- Object.prototype就組成了一個原型鏈,順着原型鏈,咱們能夠找到這個對象最開始繼承自哪一個對象,同時,原型鏈上的每個節點均可以繼承上游對象的全部屬性。繼承描述的應該是一種關係,或者一種動做。

new運算符

在前面的篇幅裏咱們知道,在JavaScript裏,對象能夠用字面量的形式與Object.create的形式來建立。可是JavaScript裏還有一種方式來建立一個對象,那就是使用new運算符。

var obj = new Object

console.log(obj) // {}

根據前面的內容,咱們可知obj繼承了Object.prototype對象上的屬性。關於new操做符,能夠看個人另外一篇專欄當咱們在JavaScript中new一個對象的時候,咱們到底在作什麼。那麼Object是什麼?

咱們來執行一下typeof Object,打印出來的是"function"。對的,Object是一個函數,準確地說,它是一個構造函數。new運算符操做的,應該是一個函數。

咱們能夠對任意函數執行new操做。可是一個函數若是被用做了構造函數來實例化對象,那咱們傾向於把它的首字母大寫。

var Foo = function(x) {
    this.x = x
}

var boo = new Foo(1)
console.log(boo, boo.x) // Foo {x: 1} 1

構造函數能讓咱們初始化一個對象,在構造函數裏,咱們能夠作一些初始化的操做。一般咱們在編寫一些JavaScript插件的時候會在全局對象上掛載一個構造函數,經過實例化這個構造函數,咱們能夠繼承它的原型對象上的全部屬性。

既然構造函數有屬於本身的原型對象,那麼咱們應該能讓另外一個構造函數來繼承他的原型對象咯?

var Human = function(name) {
    this.name = name
}
var Male = function(name) {
    Human.call(this, name)
    this.gender = 'male'
}

var jack = new Male('jack')
console.log(jack) // Male {name: "jack", gender: "male"}

咱們在構造函數內部執行了Human函數並改變了Human函數內部的this指向(其實這個this指向的是實例化以後的對象)。同時,咱們在Male的原型上定義一個本身的屬性gender,這樣,實例化出來的對象同時有了兩個屬性。

可是這個繼承完整麼?繼承是須要繼承原型的,可是jack的原型鏈上並無Human,咱們須要額外兩步。

var Human = function(name) {
    this.name = name
}
var Male = function(name) {
    Human.call(this, name)
    this.gender = 'male'
}

Male.prototype = Object.create(Human.prototype)
Male.prototype.constructor = Male

var jack = new Male('jack')
console.log(jack) // Male {name: "jack", gender: "male"}

這樣一來,咱們就能在jack的原型鏈上找到Human了。

ES6的類

其實前面一節看起來會比較晦澀,由於在ES6以前,JavaScript沒有類的概念(固然以後也沒有),可是咱們卻有「構造函數」,那上面一節的栗子就應該說是構造函數Male繼承了構造函數Human

我記得當時場面有點尷尬,你們都搓着手低着頭都不知道說點兒什麼

好在ES6裏咱們有了Class的關鍵字,這是個語法糖,本質上,JavaScript的繼承仍是基於原型的。可是,至少形式上,咱們能夠按照「類」的方式來寫代碼了。

class Human {
    constructor(name) {
        this.name = name
    }
}
class Male extends Human {
    constructor(name) {
        super(name)
        this.gender = 'male'
    }
}

var jack = new Male('jack')
console.log(jack) // Male {name: "jack", gender: "male"}

在控制檯上順着__proto__一層層往下翻,咱們會能找到class Maleclass Human,這說明咱們的繼承成功了。同時,咱們也能夠理解成「類Male繼承了類Human」,雖然在JavaScript其實並無類這個東西。

結語

其實通篇的核心仍是那句話:JavaScript的繼承是基於原型的。不少內容我沒有展開講解不少,表達了主幹便可。

引用

相關文章
相關標籤/搜索