對於有其餘面嚮對象語言開發經驗的人來講,在看到獨立的構造函數和原型時,極可能會感到很是困惑。好比在Java中(沒有Java經驗的開發者此段可忽略,只要知道後面提出的結論就行了),有類的概念(固然ES6也引入了類,咱們這裏以ES5爲基礎),好比如下代碼所示:javascript
public class Person { private String name; private int age; private String job; public Person(String name, int age, String job) { this.name = name; this.age = age; this.job = job; } public void sayName(){ System.out.println(this.name); } }
這是很是簡單的一個類,它有三個屬性,一個構造函數和一個方法。若是比較JavaScript,function Person
就至關於類,可是咱們發現,Java中的類是一個總體,而JavaScript除了function Person
,還有一個Person.prototype
,被定義成了兩部分。因此,JavaScript對於對象的封裝性仍是不夠完美,而動態原型模式正是致力於要解決這個問題,它把全部的信息都封裝在了構造函數中,經過在構造函數中初始化原型,既很好地體現了封裝性,又保持了組合使用構造函數和原型模式的特色,能夠說一箭雙鵰,很是完美。下面咱們來看一個例子:java
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != 'function'){ Person.prototype.sayName = function(){ console.log(this.name); }; Person.prototype.sayJob = function(){ console.log(this.job); }; } } var p1 = new Person('張三', 18, 'JavaScript');//sayName不存在,添加到原型 var p2 = new Person('李四', 20, 'Java');//sayName已經存在,不會再向原型添加 p1.sayName();//張三 p2.sayName();//李四
如代碼所示,第一次建立對象,執行構造函數時,判斷sayName()
是否存在,若是不存在,就把它添加到原型,使用if
判斷能夠確保只在第一次調用構造函數時初始化原型,避免了每次調用的重複聲明。函數
實際上這裏不只僅可使用sayName()
作爲判斷條件,還可使用sayJob()
,這個條件只是爲了測試原型是否已經初始化,只要是原型初始化以後應該存在的屬性或方法均可用來作爲判斷條件。工具
以前講過,原型也能夠用對象字面量來重寫,那動態原型模式可不可使用對象字面量呢?咱們來嘗試一下:開發工具
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != 'function'){ Person.prototype = { constructor: Person, sayName: function(){ console.log(this.name); } } } } var p1 = new Person('張三', 18, 'JavaScript');//sayName不存在,添加到原型 var p2 = new Person('李四', 20, 'Java');//sayName已經存在,不會再向原型添加 //p1.sayName();//Uncaught TypeError: p1.sayName is not a function p2.sayName();//李四
發現p1.sayName()
報了不是一個函數的錯誤,若是把p1.sayName()
註釋掉,p2.sayName()
能夠正常輸出李四,爲何會這樣呢?要想解釋清楚這個問題,咱們先來看一下如下代碼:測試
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; } var p1 = new Person('張三', 18, 'JavaScript'); console.log(Person.prototype);//{constructor: ƒ} Person.prototype = { constructor: Person, sayName: function(){ console.log(this.name); } } var p2 = new Person('李四', 20, 'Java'); console.log(Person.prototype);//{constructor: ƒ, sayName: ƒ} // p1.sayName();//Uncaught TypeError: p1.sayName is not a function p2.sayName();//李四
也是一樣的現象,p1.sayName()
不是一個函數。那麼p1
、p2
的區別在哪兒呢?區別就在於經過new
關鍵字建立對象的前後順序,是先於重寫原型建立,仍是後於重寫原型建立。this
咱們知道,經過new
關鍵字建立一個對象,這個對象會有一個屬性__proto__
指向相應函數的原型,這裏代碼中的p1正是指向了這個原型,在Chrome的開發工具中能夠看到,以下圖所示:prototype
可是重寫原型,就是建立了一個新對象,函數的指針Person.prototype
由引用舊的原型對象改成引用這個新對象,而舊的原型對象如今只被p1.__proto__
引用着,實例p1
和Person
原型之間的關係被切斷了,因此調用p1.sayName()
就報了不是一個函數的錯誤,由於舊原型對象上沒有sayName
方法。設計
再來看p2
,由於是先重寫原型,因此當p2
被new
出來時,p2
的__proto__
屬性指向的就是這個新原型,故而調用sayName
方法時,向上搜索原型能夠找到sayName方法
,正常輸出李四,下面的示意圖能夠直觀地表示這種狀況:指針
如今回過頭來看一開始提出的問題,爲何動態原型模式不能用對象字面量的方式重寫。第一次建立實例對象時,先new
,而後執行構造函數,重寫原型,那麼此時實例的__proto__
指向的仍是原來的原型,不是重寫後的原型。第二次建立實例,由於新原型對象已經建立好,因此實例的__proto__
指向的就是重寫的這個原型。使用給原型添加屬性的方式操做的一直是同一個原型,因此也就不存在前後的問題。
這就是動態原型模式,相比組合使用構造函數和原型模式而言,封裝性更優秀,可是一個小缺點就是不能使用對象字面量的形式初始化原型,這是須要留意的。開發者在實際應用中可根據具體狀況,靈活選擇,肯定使用哪一種方式。
本文參考《JavaScript高級程序設計(第3版)》