咱們在討(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
就組成了一個原型鏈,順着原型鏈,咱們能夠找到這個對象最開始繼承自哪一個對象,同時,原型鏈上的每個節點均可以繼承上游對象的全部屬性。繼承描述的應該是一種關係,或者一種動做。
在前面的篇幅裏咱們知道,在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以前,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 Male
跟class Human
,這說明咱們的繼承成功了。同時,咱們也能夠理解成「類Male繼承了類Human」,雖然在JavaScript其實並無類這個東西。
其實通篇的核心仍是那句話:JavaScript的繼承是基於原型的。不少內容我沒有展開講解不少,表達了主幹便可。