曾經,我寫過下面一段代碼,我滿心歡喜覺得獲得了JS面向對象編程和原型繼承的真諦。程序員
var pets = { sound: '', makeSound: function() { console.log(this.sound); } } var cat = { sound: 'miao' }; cat.prototype = pets; cat.makeSound();複製代碼
而後,我將這段代碼複製粘貼到瀏覽器的console調試工具下運行,居然報出一個錯誤。我不得不認可,原來我根本就不懂JS的面向對象編程。編程
個人目的是,讓cat繼承pets的makeSound方法,雖然cat沒有makeSound方法,可是它能夠沿着原型鏈查找到pets的makeSound方法。可是,很明顯,這段代碼有錯誤,沒法達到個人預期。我認識到,我根本就沒有弄懂過prototype
和__proto__
屬性的關係和做用。瀏覽器
若是你也不知道上面的代碼確切的錯在哪裏,那你也須要補上這一課。若是你知道上面的代碼錯在哪裏,但不知道爲何是這樣的安排,這篇文章也能讓你有收穫,如標題所言,知其然,亦知其因此然。markdown
爲了講清楚這個問題,咱們先拋開上面的錯誤,從構造一個簡單對象開始談起。函數
咱們將從一個簡單的工廠函數開始:工具
var Pets = function(sound) { var obj = { sound: sound }; obj.makeSound = function() { console.log(obj.sound); } obj.bite = function() { console.log('bite'); } return obj; } var dog = Pets('wang'); dog.makeSound(); // wang複製代碼
上面定義了一個寵物製造工廠,它生成了一個擁有sound屬性的對象,並將接收的參數賦值sound屬性。而後在該對象上添加了兩個方法,最後將這個對象返回。有了這個函數,咱們就能夠製造出各類各樣的寵物了。(爲了先後一致,請忽略函數首字母大寫的問題)性能
然而,上面的工廠函數有一個缺點。若是咱們想給這個工廠函數添加更多的方法,或者刪除多餘的方法,那麼咱們不得不改動這個函數自己的代碼。當方法變得愈來愈多的時候,這個函數就變得難以維護。因此,咱們進行以下優化:優化
var Pets = function(sound) { var obj = { sound: sound }; extend(obj, Pets.methods); // 注意,這裏的extend函數是沒有實現的。 return obj; } Pets.methods = { makeSound: function() { console.log(this.sound); }, bite: function() { console.log('bite'); } } var dog = Pets('wang'); dog.makeSound() // wang複製代碼
能夠看到,咱們給Pets函數添加了一個methods屬性,用來統一保存和維護該工廠函數的方法。當使用該函數生成obj對象時,經過一個extend函數將Pets.methods中的方法通通複製到obj中。這時,代碼本質上沒有改變什麼,只是經過形式上的改變,使得代碼更容易維護。若是咱們想給Pets工廠函數添加新的方法,能夠經過下面的方式實現,而沒必要修改函數:this
Pets.methods.scratch = function() {/*...*/}複製代碼
上面的代碼,每次調用工廠函數生成的新對象,都有一份對Pets.methods中方法的徹底複製。這種建立對象的方式是低效的,既然Pets.methods中的方法是全部由工廠函數建立的對象都擁有的,咱們其實並不但願每一個對象都保留一份複製,而是但願經過某種方式,讓全部的對象共享方法,因此就有了繼承的概念。在JS中,Object.create
函數能夠實現繼承的目的,咱們將代碼改寫以下:spa
var Pets = function(sound) { var obj = Object.create(Pets.methods); obj.sound = sound; return obj; } Pets.methods = { makeSound: function() { console.log(this.sound); }, bite: function() { console.log('bite'); } } var dog = Pets('wang'); dog.makeSound(); // wang dog.bite(); // bite複製代碼
Object.create
構建了一個繼承關係,即obj繼承了Pets.methods的方法。obj內部有一個[[Prototype]]
指針,指向了Pets.methods,Pets.methods也就成了該對象的原型對象。[[Prototype]]
指針是一個內部屬性, 腳本中沒有標準的方式訪問它,可是在Chrome、 Safari、Firefox中支持一個屬性__proto__
,而在其餘瀏覽器實現中,這個屬性都是徹底不可見的。在Chrome的調試窗口打印dog:
能夠看到,dog並不擁有makeSound方法,但仍然可使用該方法,由於它能夠沿着__proto__
指針指明的方向繼續查找makeSound方法,一旦找到同名方法就返回該方法。(任何對象都繼承自Object對象,因此方法查找的終點在Object處,假如查找到達Object對象且Object對象也沒有該方法,則返回undefined)
上面的改進,經過繼承,將對象的公用方法委託給原型對象,每次建立新的對象時,就免去了屬性的複製,提升了代碼的性能和可維護性。下面,咱們對代碼進行一點小改動:
var Pets = function(sound) { var obj = Object.create(Pets.prototype); obj.sound = sound; return obj; } Pets.prototype.makeSound = function() { console.log(this.sound); } Pets.prototype.bite = function() { console.log('bite'); } var dog = Pets('wang'); dog.makeSound(); // wang dog.bite(); // bite複製代碼
咱們把做爲原型對象的Pets.methods換了一個名稱,叫作Pets.prototype
。是否是以爲哪裏不對?怎麼能這麼隨意的替換呢?prototype
但是JS語言中很特殊的一個屬性,有着某種很特別的功能,怎麼可能和這裏的methods同樣呢?沒錯,這麼替換,而不是一開始就使用prototype
,就是想說明,其實,prototype
屬性並無什麼神祕的地方,它的做用和這裏的methods幾乎是同樣的。
上面的這種建立對象,並將對象方法委託到原型對象的方式,在JS編程中是如此的常見,因此語言自己提供了一個方法,將重複的部分自動處理,程序員只須要關注每一個對象不相同的部分,這個方法就是,構造函數:
var Pets = function(sound) { this.sound = sound; } Pets.prototype.makeSound = function() { console.log(this.sound); } Pets.prototype.bite = function() { console.log('bite'); } var dog = new Pets('wang'); dog.makeSound(); // wang dog.bite(); // bite var cat = new Pets('miao'); cat.makeSound(); // miao cat.bite(); // bite複製代碼
構造函數的new
操做,自動處理了繼承和返回操做。能夠這麼理解new
的主要做用:
var Pets = function(sound) { /* this = Object.create(Pets.prototype); */ this.sound = sound; /* return this; */ }複製代碼
就好像在執行new操做的時候,語言自動處理了註釋部分的代碼,只須要咱們關注將要建立的對象的特殊部分便可。(固然,上面的代碼去掉註釋是沒法運行的,由於this是隻讀的,不能賦值,瀏覽器運行會報錯。但原理是正確的。)
prototype
則爲構造函數的一個屬性,也是由構造函數所建立對象的原型對象。若是必定要說prototype和前面例子中的methods有什麼不一樣,那就是,prototype
有一個默認屬性constructor
,該屬性指向構造函數自己。
console.log(Pets.prototype.constructor === Pets) // true複製代碼
順便,你認爲下面的表達式應該打印什麼?
console.log(dog.constructor)複製代碼
應該是Pets,dog自身沒有constructor
屬性,因此沿着原型鏈向上查找,找到Pets.prototype,而Pets.prototype是有這個屬性的,返回這個屬性,該屬性指向構造函數Pets,因此打印Pets。
到這裏,關於原型繼承中涉及到的構造函數、prototype
、constructor
,[[Prototype]]
以及建立出來的對象之間的關係已經所有呈現出來了。來作個總結:
prototype
是構造函數的一個屬性,並無什麼特殊和神祕的性質。prototype
是由構造函數所建立對象的原型對象,對象的公共方法和屬性能夠委託到prototype。prototype
之間。(Object.create
創建了對象和原型之間的繼承關係,和構造函數沒有關係)constructor
是語言自動賦予prototype
的一個屬性,其值爲構造函數自己。[[Prototype]]
是對象的一個內部屬性,是一個指針,指向對象的原型對象,在Safari、Chrome和Firefox下,能夠經過__proto__
屬性訪問。仍是使用上面的例子,咱們將全部這些關鍵詞之間的相互關係使用一個圖示展現出來:
如今回過頭去看開頭提到的那個錯誤例子,簡直就錯的離譜啊。這個錯誤明顯神化了prototype
的做用,覺得只要使用了prototype
屬性,而後就如同黑魔法通常,在兩個徹底不相關的對象之間架起了一座橋樑,也就是繼承關係,而後就能夠隨意使用另一個對象的方法了。天真!
個人問題在於,首先,神話了prototype
的做用。prototype
並無這種黑魔法,它只是一個屬性。
其次,沒有搞明白繼承關係到底存在與哪兩個對象之間。(被建立對象和prototype之間)
因此,在錯誤的代碼中,dog對象沒有makeSound方法,dog對象繼承Object.prototype
,而非pets,而Object.prototype
上並無所謂的makeSound方法,返回undefined,因此報錯。
以上,但願對你有所幫助。