我來從新學習 javascript 的面向對象(part 2)

續上一集內容,經過構造函數的方式,成功地更新了生產技術,老闆笑呵呵,工人少奔波,只是問題總比辦法多,又遇到一個新問題,就是會形成一些資源的重複和浪費,那麼通過工程師們的智慧交流,他們產生了一個新技術,原型模式javascript

1、使用原型模式

function Food() {}

Food.prototype.name = "蘋果";
Food.prototype.sayName = function() {
  console.log("我是" + this.name);
};

var food1 = new Food();
food1.sayName();

var food2 = new Food();
food2.sayName();
// 建立無限多的 food 。。。。。。

console.log(food1.sayName == food2.sayName); // 返回 true
  • 將全部屬性和方法,包括sayName 方法都放到原型Food的原型上去
  • 跟以前構造函數建立新對象的方式同樣,使用new來建立

這樣就完成了原型模式的使用了,可以將函數進行共享,不用每次都重複建立不一樣的函數實例了,並且全部的屬性共享,也可以很方便節省代碼和簡化結構,其餘人也能夠很方便地進行使用。html

可是比較懵逼,爲何這樣就能夠了呢?原型是個什麼東西?怎麼起做用的呢?java

1.1 理解什麼是原型

在《javascript 高級程序設計》裏面是這樣說的,咱們建立的每一個函數都有一個 prototype,這個屬性是一個內存指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。node

換句大白話來講:編程

  • 原型就是根,全部東西都有根,是來自於哪裏,是被誰創造出來的,而且可以經過這個根去追溯父輩祖輩的信息。
  • 所有東西都會由原型創造,因此都會帶有一個原型屬性,只是不一樣的原型創造出來的東西帶有不一樣的原型屬性。

例如圖1,能夠粗獷地理解爲蛋是雞生的,因此蛋的原型是雞。

圖片引用來自:https://hackernoon.com/unders...windows

或者用親屬關係來理解,原型就是你的父輩祖輩。

圖片引用自http://china.findlaw.cn/info/...瀏覽器

例如圖2,這是一個類樹狀結構的組織:

圖片引用自:http://rohitnsit08.blogspot.c...函數

  • 這裏的global object 的意思在後面有解釋。
  • 從這個圖能夠看到,各類不一樣過的對象,都會有不一樣的原型,string 的原型就是 string.prototypefunction 的原型就是 string.prototype 等等,而這些原型的原型就是Object.prototype了,因此就有那麼一句話,在 javascript 裏面,全部的東西都是對象

在 javascript 裏面,global object 有4種:學習

  1. 在瀏覽器裏面,windows 被稱做是 global object
  2. 在 nodejs 裏面,nodejs 的運行自己也是一個 global objec
  3. 在 Worker 線程下, WorkerGlobalScope 也叫 global object
  4. 在通常 javascript 運行過程當中,在全部對象被建立以前,會預先建立一個 global object,裏面包含了全部這個 javascript 引擎裏面擁有的屬性和方法,這個也叫作 global object,而且 javascript 的對象系統都是基於這個 global object 創建的。

1.2 理解什麼是 constructor 和構造函數的 prototype 和 [[prototype]]

其實原型是很好理解的東西,就是原來的形態,例如string 的原型就是 string.prototype,字符串的原來的形態就是字符串原型,可是還有一些比較影響理解和學習的東西,例如constructor prototype [[prototype]] 這些。網站

① 先來講constructor

其實構造函數也有constructor,原型對象有constructor,實例有constructor 也有,或者更加籠統的說,全部對象都是有constructor的。

// 構造函數的constructor,默認構造函數的原型對象是Function對象
function Food() {}
console.log(Food.constructor); // 返回[Function: Function]

// 實例的constructor是Food
var food1 = new Food("蘋果");
console.log(food1.constructor); // 返回 [Function: Food]

// 構造函數的原型對象Food.prototype 也有constructor,指向構造函數
console.log(Food.prototype.constructor); // 返回 [Function: Food]
  • 構造函數也是函數,他的 constructor默認會指向[Function: Function],也就是函數原型對象是他的原型對象。
  • 當使用 new 來實例化對象的時候,實例的constructor會默認指向構造函數,證實是哪個來自於哪個構造函數,可是僅此而已,只是一個標識,因此是 Food
  • 構造函數 的原型對象 Food Prototype上的constructor會指向構造函數 Food
能夠看出,經過 constructor 能夠看到他們之間的關係,可是經過 constructor鏈接的關係是很脆弱的 (容易變化,不可靠),也由於 javascript 在設計當初並無太多考慮這個狀況,因此 constructor比較雞肋。

另外雖然有這麼多constructor,可是咱們通常討論的比較多的是,原型對象的constructor,他會默認指向構造函數的 prototype 屬性,僅此而已,若是他沒有被改變的話,也能夠充當一種標誌,表明經過這個構造函數生成的實例是來自哪一個原型的,從而判斷對象的類型是哪種(但不可靠)

舉例說明:

function Food() {}
console.log(Food.constructor); // 返回[Function: Function]

// 當重寫原型的時候,實例的constructor 就變成了[Function: Object]
Food.prototype = {
  name: "蘋果"
};
var food2 = new Food("蘋果");
console.log(food2.constructor); // 返回 [Function: Object]

這裏實例的constructor就被改變了,因此通常咱們能夠看到重寫原型的時候(原型鏈被切斷,會默認指向默認的原型對象),會手動加入一個 constructor 屬性來指定它的值,以方便識別。

function Food() {}
console.log(Food.constructor); // 返回[Function: Function]
// 咱們須要主動標記 constructor屬性
Food.prototype = {
  constructor: Food,
  name: "蘋果"
};
var food2 = new Food("蘋果");
console.log(food2.constructor); // 返回 [Function: Food]
constructor其實沒有什麼用處,他是JavaScript語言設計的歷史遺留物。因爲 constructor屬性是能夠變動的,因此未必真的指向對象的構造函數,只是一個提示。不過,從編程習慣上,咱們應該儘可能讓對象的 constructor指向其構造函數,以維持這個慣例。--by 賀師俊

② 再來講prototype

在 javascript 裏面,只要是函數,都會有這樣一個屬性prototype,這個屬性指向函數的原型對象,在默認狀況下,全部原型對象都會自動得到一個 constructor 屬性,指向prototype 所在的函數。

function Food() {}
console.log(Food.prototype) // 返回Food {}

var food1 = new Food("蘋果");

console.log(food1.prototype) // 返回 undefined
  • 構造函數 Food 的prototype屬性是一個內存指針,最終是指向 Food 的原型對象 Food Prototype 的,這裏須要注意個人寫法,是 Food Prototype ,這裏雖然很類似,但其實不是。
  • 實例對象上的 prototype 屬性沒辦法直接查看,因此返回 undefined,須要用別的方法來查看。

③ 最後說說[[prototype]]__proto__

這是存在於實例身上的 prototype 屬性,可是沒辦法直接查看,只能經過某些方式來獲取和判斷。不一樣的瀏覽器有不一樣的叫法,的屬性名字也多是[[prototype]] 或者_proto_

// Object.getPrototypeOf會返回原型對象,但看起來跟普通構造函數沒區別
console.log(Object.getPrototypeOf(food1)); // 返回 Food {}

// 因此通常會使用這個方式來判斷原型對象是否一致
console.log(Object.getPrototypeOf(food1) === Food.prototype); // 返回 true
// 或者這個方式,isPrototypeOf會直接判斷
console.log(Food.prototype.isPrototypeOf(food1)) // 返回 true

1.3 爲何可以經過原型模式來解決問題呢?

原型的做用主流程:

  1. 在 javascript 裏面,建立一個新函數(對象),都會在建立過程裏面增長一個prototype屬性,也就是原型屬性,這個屬性指向構造函數的原型對象,例如food1prototype屬性指向Food的原型對象Food prototype
  2. 而這個被指向的原型對象裏面也會自動得到一個constuctor構造函數屬性,這個屬性裏面包含了一個指向,指向以前被建立的對象的prototype屬性的所在位置,至關於原型對象是母體,被建立的對象會關聯到母體身上,
  3. javascript 解析器讀取到對象以後,會執行一次搜索,若是在當前對象上沒有搜索到目標屬性的話,就會繼續搜索指針指向的原型對象,會不斷逐級查找(原型對象),直至找到爲止。

參考前面所說的原型,constructor 和 prototype 的內容來理解

在《javascript 高級程序設計》第三版裏面的有一幅圖:

  • person1 和 person2 都是實例, Person 是構造函數,Person Prototype 是 Person 構造函數的原型對象。
  • person1或者 person2 的[[Prototype]]都指向了Person Prototype

問什麼這裏會有一條線指向了構造函數?

由於Person 構造函數的prototype 指向了Person Prototype,而Person Prototypeconstructor 也指向了Person 構造函數,他們之間經過這樣來互相確認「關聯狀態」,但僅僅互相確認關係而已(由於 constructor 容易被改變)。

類比到咱們的 Food 例子裏面去,food1和 FoodFood Prototype的關係就跟 person1和 person2和 PersonPerson Prototype 的關係是同樣的。

2、 對於原型的一些使用技巧

① 若是須要查找這個實例對象的原型的話,可使用Object.getPrototypeOf ,他會返回整個原型對象。

function Food() {}

Food.prototype.name = "蘋果";

Food.prototype.sayName = function() {
  console.log("我是" + this.name);
};

var food1 = new Food();
console.log(Object.getPrototypeOf(food1)) // 返回 Food { name: '蘋果', sayName: [Function] }

② 只能經過對象實例訪問保存在原型的值,不能經過對象實例來重寫原型中的值。
③ 對象實例能夠重寫從原型對象中「繼承」過來的同名屬性,這時候會切斷對象實例和原型對象的某個同名屬性的聯繫,若是想恢復聯繫即恢復沒改過的同名屬性的話,可使用delete刪除對象實例的某個屬性。
hasOwnProperty()方法能夠檢測一個屬性是存在於實例中仍是存在於原型中。

function Food() {}

Food.prototype.name = "蘋果";

Food.prototype.sayName = function() {
  console.log("我是" + this.name);
};

var food1 = new Food();

console.log(food1.hasOwnProperty("name")); // 返回 false
food1.name = "bilibili"; // 設置 food1的 name 屬性(也就是改寫從原型對象繼承過來的 name 屬性)
console.log(food1.hasOwnProperty("name")); // 返回 true
console.log(food1.name); // 返回 bilibili

⑤ 更簡單的原型寫法

function Food() {}

Food.prototype = {
  constructor: Food, // 這裏須要注意
  name: '蘋果',
};
  • 若是不寫constructor的話,Food.prototype constructor就再也不指向 Food ,這樣就沒辦法經過constructor來識別獲得改對象實例是屬於哪一個原型對象了。
  • 以這種方式編寫原型的時候,若是主動設置constructor,對象的[[Enumerable]] 可遍歷屬性就會被設置爲 true,表明能夠被遍歷。

⑥ 在原型對象上直接編輯修改,會即時反應到實例對象上,因此能夠隨時進行修改,很方便。
⑦ 若是重寫原型對象,要注意原型對象的指向問題:

// 原型鏈會被切斷
function Food() {
}
var food1 = new Food("蘋果"); // 繼續指向原來的 Food.prototype(最初的那個原型對象)
// 重寫Food.prototype
Food.prototype = {
  constructor: Food,
  name: '蘋果',
};
console.log(food1.name); // 返回 undefined
// 原型鏈不會被切斷
function Food() {
}
// 先重寫Food.prototype
Food.prototype = {
  constructor: Food,
  name: '蘋果',
};
// 再實例化對象
var food1 = new Food("蘋果"); // 指向新的被重寫後的Food.prototype
console.log(food1.name); // 返回 蘋果

在 new 建立完實例以後,實例的原型對象是構造函數的原型對象,若是在這時候重寫了構造函數的原型對象的話,那麼原來實例跟原來構造函數的原型對象的連接就會被切斷,就沒法使用原型對象上的數據了。

3、文末咱們又遇到了新問題了

用了原型模式以後,雖然解決了遇到的一系列問題,但也帶來了一些新的反作用(怎麼反作用那麼多。。。。。),原型模式的共享特性帶來了方便之餘,也形成了一些困擾,若是咱們須要一些不想共享的信息,例如 food1 的原產地是巴西,印度,非洲,food2的原產地是巴西,印度,俄羅斯,他們之間有一些區別,不能徹底共享,那麼怎麼辦呢?

會經過組合使用構造函數模式和原型模式或者動態原型模式來解決,下回分解。

參考內容

  1. 紅寶書,《javascript 高級程序設計第三版》

版權信息

做者: 慫如鼠
網站: https://www.whynotbetter.com 本做品著做權歸做者全部,商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索