面向對象的語言能夠經過類建立任意多個具備相同屬性和方法的對象。ECMAScript中沒有類的概念(在ES6的class以前),可是它的 對象是基於引用類型建立的,能夠在必定程度上充當"類「的角色。設計模式
JavaScript建立對象最經常使用的方法是使用 Object構造函數(即經過new Object())或 對象字面量。但它們有個明顯缺點:若是建立的一堆對象中都含有相同的方法,那麼就會重複寫大量重複的代碼。因此會採用 工廠模式、構造函數模式 、原型模式等設計模式來建立對象。其中 原型模式是本文須要着重分析的。數組
Eg1:函數
function Person() { } Person.prototype.name = "Bonnie"; Person.prototype.age = 26; Person.prototype.sayName = function() { console.log(this.name) } var person1 = new Person(); var person2 = new Person(); console.log(person1.sayName === person2.sayName);//true
構造函數是專門用來建立對象的函數,其自己也是函數。構造函數始終都應該以一個大寫字母開頭。測試
要使用構造函數建立新實例,必須使用new操做符。ui
不管什麼時候何地,只要建立了一個函數,那麼該函數就會得到一個prototype屬性,這個prototype屬性是一個指針,指向該函數的 原型對象。this
原型對象的用途是它包含了能夠由 特定類型的全部實例(即由同一個構造函數建立的實例)共享的屬性和方法。prototype
默認狀況下,原型對象都會自動得到一個constructor屬性,這個constructor屬性是一個指針,指向構造函數。設計
實例是經過構造函數經過new操做符建立的對象。指針
經過構造函數建立的實例會包含一個__proto__屬性,這個__proto__屬性是一個指針,指向構造函數的 原型對象。code
NOTE: 這個屬性其實被稱爲[[Prototype]],在js中無標準方式訪問。在Firefox、Safari、Chrome中js能夠經過__proto__屬性訪問,在其它地方沒有辦法訪問。
構造函數、原型對象、實例的關係能夠用下圖表示:
具體到上Eg1,能夠用下圖表示:
若是在實例中添加了一個屬性,該屬性與原型中的一個屬性同名,那麼就會在該實例中建立該屬性,該屬性會屏蔽原型中的同名屬性,但 不會修改原型中的同名屬性,即便將該屬性設爲null。
使用delete操做符刪除實例中的屬性後,實例就恢復了對原型中同名屬性的鏈接。
Eg2:
function Person() { } Person.prototype.name = "Bonnie"; Person.prototype.age = 26; Person.prototype.sayName = function() { console.log(this.name) } var person1 = new Person(); var person2 = new Person(); person1.name = 'Summer'; console.log(person1.name);//'Summer' console.log(person2.name);//'Bonnie'
Eg3:
function Person() { } Person.prototype = { name:'Bonnie', age:26, sayName:function() { console.log(this.name); } } var person3 = new Person(); person3.constructor === Person;//false NOTE:實例會繼承原型對象上的constructor方法。
以下從新調整constructor:
Person.prototype.constructor = Person; person3.constructor === Person;//true //可是這樣會使constructor屬性變爲可枚舉屬性 Object.getOwnPropertyNames(Person.prototype);//["name", "age", "sayName", "constructor"] //Object.getOwnPropertyNames包含不可枚舉屬性 Object.keys(Person.prototype);// ["name", "age", "sayName", "constructor"] //Object.keys不包含不可枚舉屬性
更好的辦法是這樣調整constructor:
Object.defineProperty(Person.prototype, 'constructor',{ enumerable: false, value: Person }); person3.constructor === Person;//true Object.getOwnPropertyNames(Person.prototype);//["name", "age", "sayName", "constructor"] Object.keys(Person.prototype);//["name", "age", "sayName"]
對原型對象在任什麼時候候所作的任何修改都能當即從實例中反映出來。
先建立實例再修改原型:
Eg4:
var person4 = new Person(); person4.sayHello();//Uncaught TypeError:person4.sayHello is not a function Person.prototype.sayHello = function() { console.log('hello!'); } person4.sayHello();//hello
可是 若是對原型對象進行整個重寫,那麼就切斷了實例與原型對象的聯繫,即 **實例的__proto__指針再也不指向原型對象**。
Eg5:
var person5 = new Person(); person5.sayHi();//Uncaught TypeError:person4.sayHi is not a function Person.prototype = { name:'Bonnie', age:26, sayName: function() { console.log(this.name) }, sayHi: function() { console.log('hi~'); } } person5.sayHi();//Uncaught TypeError:person4.sayHi is not a function
若是共享的是方法,那麼很是合適;
若是共享的是基本值,那麼經過在實例上添加同名屬性,能夠覆蓋原型中的同名屬性;
可是若是共享的是引用類型值(如數組),那麼若是在一個實例上修改,也會影響到其餘屬性。
例如共享的是數組:
Eg6:
function Person() { } Person.prototype.name = "Bonnie"; Person.prototype.age = 26; Person.prototype.sayName = function() { console.log(this.name) } Person.prototype.friends = ['Summer','Huiyun']; var person1 = new Person(); var person2 = new Person(); person1.friends.push('Angela');//若是直接push修改,會影響其餘實例 console.log(person1.friends);//['Summer','Huiyun','Angela']; console.log(person2.friends);//['Summer','Huiyun','Angela']; person1.friends = ['Angela'];//若是直接賦值覆蓋,則不會影響其餘實例 console.log(person1.friends);//['Angela']; console.log(person2.friends);//['Summer','Huiyun','Angela'];
爲了解決這個問題,將構造函數模式和原型模式組合起來使用將是個好辦法。
組合使用構造函數模式和原型模式:
Eg6:
function Person(name,age) { this.name = name; this.age = age; this.friends = ['Tom', 'Lulu']; } Person.prototype.sayName = function () { console.log(this.name); } var person6 = new Person('Kate', 18); var person7 = new Person('Bob', 28); console.log(person6.friends);//["Tom", "Lulu"] console.log(person7.friends);//["Tom", "Lulu"] person6.friends.push('Linda'); console.log(person6.friends);// ["Tom", "Lulu", "Linda"] console.log(person7.friends);//["Tom", "Lulu"]
這樣每一個實例都有一份本身的屬性副本,又共享着對方法的引用。
驗證某原型對象是否爲某實例的原型對象。
Person.prototype.isPrototypeOf(person1);// true
獲取實例的原型對象。
Object.getPrototypeOf(person1) === Person.prototype;//true
檢測一個屬性是否存在於實例自己中,仍是存在於原型中。
person1.hasOwnProperty('name');//true person2.hasOwnProperty('name');//false
經過對象可以訪問給定的屬性,則返回true,不管該屬性是在實例仍是在原型中, 不管該屬性是否可枚舉。
'name' in person1;//true 'name' in person2;//true
for...in循環返回對象全部的 可枚舉 屬性,不管該屬性是在實例仍是在原型中。
for (let prop in person1) { console.log(`${prop}: ${person1[prop]}`); } /** 輸出: name: Summer age: 26 sayName: function() { console.log(this.name) } */
取得對象上全部的 可枚舉 的 實例 屬性組成的數組
Object.keys(person1);//["name"] Object.keys(person2);//[]
獲取對象的全部 實例 屬性,不管該屬性是否可枚舉。
方法 | 是否包含不可枚舉屬性 | 是否查找prototype鏈 |
---|---|---|
hasOwnProperty | √ | × |
in | √ | √ |
for...in | × | √ |
Object.keys() | × | × |
Object.getOwnPropertyNames() | √ | × |
ECMAScript將 原型鏈 做爲實現繼承的主要方法。
原型鏈的基本思想就是 讓一個引用類型繼承另外一個引用類型的屬性和方法。參考構造函數、原型對象、實例直接的關係,原型鏈的造成就是讓 一個類型的實例 做爲 另外一個類型 的 原型對象。
function SuperType() { this.superProperty = true; } SuperType.prototype.getSuperValue = function() { return this.superProperty; } function SubType() { this.subProperty = false; } SubType.prototype = new SuperType(); //將 SuperType類型的實例 做爲 SubType類型的 原型對象, 這樣就重寫了SubType的原型對象(沒有使用SubType默認的原型對象), 實現了繼承。 SubType.prototype.getSubValue = function() { return this.subProperty; } const subTypeInstance = new SubType(); console.log(subTypeInstance.getSuperValue());//true
這樣SubType的實例(即subTypeInstance)的__proto__指向SuperType的實例(即SubType的原型),SuperType的實例的__proto__又指向SuperType的原型。
NOTE:
全部函數的默認原型都是Object類型的實例,故 SuperType的原型對象(即SuperType.prototype)就是Object類型的實例,其__proto__指向的Object類型的原型對象。比較繞
上例能夠總結爲,SubType繼承了SuperType,SuperType繼承了Object。
instanceof能夠測試某個實例的類型和其繼承的類型。
subTypeInstance instanceof SubType;//true subTypeInstance instanceof SuperType;//true subTypeInstance instanceof Object;//true
isPrototypeOf()也能夠測試某個實例的原型鏈中出現過的原型對象
SubType.prototype.isPrototypeOf(subTypeInstance);//true SuperType.prototype.isPrototypeOf(subTypeInstance);//true Object.prototype.isPrototypeOf(subTypeInstance);//true
function SuperType() { this.superProperty = true; } SuperType.prototype.getSuperValue = function() { return this.superProperty; } function SubType() { this.subProperty = false; } SubType.prototype = new SuperType(); //*1 替換SupType的默認原型對象爲 SuperType的實例,實現繼承 SubType.prototype.getSubValue = function() { return this.subProperty; } SubType.prototype.getSuperValue = function() { //*2 在原型對象替換爲新的以後,再給繼承的原型對象添加/重寫方法 return 'newSuperValue'; } var subTypeInstance = new SubType(); var superTypeInstance = new SuperType(); subTypeInstance.getSuperValue();//'newSuperValue' 重寫原型對象中已經存在的方法後會屏蔽原型對象的舊方法 superTypeInstance.getSuperValue();//true 重寫原型對象中已經存在的方法後,對於原型對象的類型自己的實例沒有影響
若是將2 放在 1以前,則重寫不能生效:
function SuperType() { this.superProperty = true; } SuperType.prototype.getSuperValue = function() { return this.superProperty; } function SubType() { this.subProperty = false; } SubType.prototype.getSuperValue = function() { //在原型對象替換爲新的以後,再給繼承的原型對象添加/重寫方法 return 'newSuperValue'; } SubType.prototype = new SuperType(); //替換SupType的默認原型對象爲 SuperType的實例,實現繼承 SubType.prototype.getSubValue = function() { return this.subProperty; } var subTypeInstance = new SubType(); var superTypeInstance = new SuperType(); subTypeInstance.getSuperValue();//true superTypeInstance.getSuperValue();//true
若是使用對象字面量的方式重寫,則會重寫整個原型鏈,以前所作的對原型對象的替換就是無效的。
function SuperType() { this.superProperty = true; } SuperType.prototype.getSuperValue = function() { return this.superProperty; } function SubType() { this.subProperty = false; } SubType.prototype = new SuperType(); //*1 替換SupType的默認原型對象爲 SuperType的實例,實現繼承 SubType.prototype = { //對象字面量的方式重寫,是的*1行無效 getSubValue: function() { return this.subProperty; }, getSuperValue: function() { return 'newSuperValue' } } var subTypeInstance = new SubType(); subTypeInstance.getSuperValue();//'newSuperValue' console.log(subTypeInstance instanceof SuperType);//false
function SuperType() { this.superProperty = true; this.colors = ['red', 'yellow']; } SuperType.prototype.getSuperValue = function() { return this.superProperty; } function SubType() { this.subProperty = false; } SubType.prototype = new SuperType(); var subTypeInstance = new SubType(); var subTypeInstance2 = new SubType(); var superTypeInstance = new SuperType(); subTypeInstance.colors.push('green');//*1 console.log(subTypeInstance.colors);//["red", "yellow", "green"] console.log(subTypeInstance2.colors);//["red", "yellow", "green"] console.log(superTypeInstance.colors);//["red", "yellow"]
交換1和 2 行的位置,結果依然同樣:
function SuperType() { this.superProperty = true; this.colors = ['red', 'yellow']; } SuperType.prototype.getSuperValue = function() { return this.superProperty; } function SubType() { this.subProperty = false; } SubType.prototype = new SuperType(); var subTypeInstance = new SubType(); subTypeInstance.colors.push('green');//*2 var subTypeInstance2 = new SubType(); var superTypeInstance = new SuperType();//*1 console.log(subTypeInstance.colors);//["red", "yellow", "green"] console.log(subTypeInstance2.colors);//["red", "yellow", "green"] console.log(superTypeInstance.colors);//["red", "yellow"]
《JavaScirpt高級程序設計》6.2.3,6.3.1
《你不知道的JavaScript(上)》Chapter5