原型與原型鏈.

1、建立對象的重要模式: 原型模式

面向對象的語言能夠經過類建立任意多個具備相同屬性和方法的對象。ECMAScript中沒有類的概念(在ES6的class以前),可是它的 對象是基於引用類型建立的,能夠在必定程度上充當"類「的角色。設計模式

JavaScript建立對象最經常使用的方法是使用 Object構造函數(即經過new Object())或 對象字面量。但它們有個明顯缺點:若是建立的一堆對象中都含有相同的方法,那麼就會重複寫大量重複的代碼。因此會採用 工廠模式構造函數模式原型模式等設計模式來建立對象。其中 原型模式是本文須要着重分析的。數組

1.原型模式基本實現

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

2. 原型模式相關概念理解

(1)構造函數

構造函數是專門用來建立對象的函數,其自己也是函數。構造函數始終都應該以一個大寫字母開頭。測試

要使用構造函數建立新實例,必須使用new操做符。ui

(2)prototype屬性:屬於構造函數,指向原型對象

不管什麼時候何地,只要建立了一個函數,那麼該函數就會得到一個prototype屬性,這個prototype屬性是一個指針,指向該函數的 原型對象this

(3)原型對象

原型對象的用途是它包含了能夠由 特定類型的全部實例(即由同一個構造函數建立的實例)共享的屬性和方法prototype

(4)constructor屬性:屬於原型對象,指向構造函數

默認狀況下,原型對象都會自動得到一個constructor屬性,這個constructor屬性是一個指針,指向構造函數。設計

(5)實例

實例是經過構造函數經過new操做符建立的對象。指針

(6)proto:屬於實例,指向原型對象

經過構造函數建立的實例會包含一個__proto__屬性,這個__proto__屬性是一個指針,指向構造函數的 原型對象code

NOTE: 這個屬性其實被稱爲[[Prototype]],在js中無標準方式訪問。在Firefox、Safari、Chrome中js能夠經過__proto__屬性訪問,在其它地方沒有辦法訪問。

構造函數、原型對象、實例的關係能夠用下圖表示:

具體到上Eg1,能夠用下圖表示:

3.原型模式相關規律

(1)實例屬性會屏蔽原型屬性

若是在實例中添加了一個屬性,該屬性與原型中的一個屬性同名,那麼就會在該實例中建立該屬性,該屬性會屏蔽原型中的同名屬性,但 不會修改原型中的同名屬性,即便將該屬性設爲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'

(2) 重寫整個原型對象,原型對象會喪失constructor屬性的指向

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"]

(3)原型的動態性

對原型對象在任什麼時候候所作的任何修改都能當即從實例中反映出來。

先建立實例再修改原型:

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

(4)原型模式具備共享本性,不但共享方法,也會共享屬性(不合理)

若是共享的是方法,那麼很是合適;

若是共享的是基本值,那麼經過在實例上添加同名屬性,能夠覆蓋原型中的同名屬性;

可是若是共享的是引用類型值(如數組),那麼若是在一個實例上修改,也會影響到其餘屬性。

例如共享的是數組:

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"]

這樣每一個實例都有一份本身的屬性副本,又共享着對方法的引用。

4. 原型模式相關方法

(1)原型對象.isPrototypeOf(實例)

驗證某原型對象是否爲某實例的原型對象。

Person.prototype.isPrototypeOf(person1);// true

(2)Object.getPrototypeOf(實例)

獲取實例的原型對象。

Object.getPrototypeOf(person1) === Person.prototype;//true

(3)實例.hasOwnProperty(屬性)

檢測一個屬性是否存在於實例自己中,仍是存在於原型中。

person1.hasOwnProperty('name');//true
person2.hasOwnProperty('name');//false

(4) in

經過對象可以訪問給定的屬性,則返回true,不管該屬性是在實例仍是在原型中, 不管該屬性是否可枚舉

'name' in person1;//true
'name' in person2;//true

(5) for...in

for...in循環返回對象全部的 可枚舉 屬性,不管該屬性是在實例仍是在原型中

for (let prop in person1) {
  console.log(`${prop}: ${person1[prop]}`);
} 
/** 輸出:
name: Summer
age: 26
sayName: function() {
  console.log(this.name)
}
*/

(6) Object.keys(對象)

取得對象上全部的 可枚舉實例 屬性組成的數組

Object.keys(person1);//["name"]
Object.keys(person2);//[]

(7) Object.getOwnPropertyNames(對象)

獲取對象的全部 實例 屬性,不管該屬性是否可枚舉

方法(3)~(7)特色對比總結

方法 是否包含不可枚舉屬性 是否查找prototype鏈
hasOwnProperty ×
in
for...in ×
Object.keys() × ×
Object.getOwnPropertyNames() ×

2、實現繼承的主要範式:原型鏈

ECMAScript將 原型鏈 做爲實現繼承的主要方法。

原型鏈的基本思想就是 讓一個引用類型繼承另外一個引用類型的屬性和方法。參考構造函數、原型對象、實例直接的關係,原型鏈的造成就是讓 一個類型的實例 做爲 另外一個類型原型對象

1.原型鏈基本實現

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:

  • 這時候subTypeInstance的constructor指向的是SuperType而非SubType,由於SubType的prototype被重寫爲SuperType的實例。

2. 實現原型鏈的相關注意點

(1) 全部函數的默認原型對象都是Object類型的實例

全部函數的默認原型都是Object類型的實例,故 SuperType的原型對象(即SuperType.prototype)就是Object類型的實例,其__proto__指向的Object類型的原型對象。比較繞

上例能夠總結爲,SubType繼承了SuperType,SuperType繼承了Object。

(2) instanceof和isPrototypeOf() 測試實例的原型鏈

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

(3)子類型給繼承的原型對象添加/重寫方法 必須在 子類型的原型對象已經替換爲新的原型對象 以後

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

(4)子類型實例對繼承的原型對象添加/重寫方法,不能使用對象字面量的方式

若是使用對象字面量的方式重寫,則會重寫整個原型鏈,以前所作的對原型對象的替換就是無效的。

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

(5)子類型實例對繼承的原型對象修改引用類型值,會影響其餘全部子類型實例的屬性,可是不會影響原型鏈上其餘被繼承類型的實例的屬性

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

相關文章
相關標籤/搜索