我來從新學習js的面向對象(part 4)

我來從新學習js 的面向對象(part 4)

續上一篇,隨着業務愈來愈大,要考慮一些繼承的玩意了,大千世界,各類東西咱們要認識和甄別是須要靠大智慧去分門別類,生物學中把動植物按界、門、綱、目、科、屬、種進行分類的方法多是最有表明的實例之一.........javascript

說人話就是,咱們終於要學習繼承的知識了,而後用這些知識去解決老闆的問題。

1、繼承-原型鏈

繼承是 OOP 開發中的一個極爲重要的概念,而在javascript 裏面,實現繼承的方式主要依靠原型鏈來實現的。


圖片來自:https://www.lieyuncj.com/p/3087html

圖一,一環扣一環,造成了鏈條,能夠適當幫助理解原型鏈的概念,原型鏈,換言之就是原型對象構成的鏈。java


圖片來源於:https://hackernoon.com/unders...node

回顧一下,構造函數,原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針,當咱們將原型對象等於另一個類型的實例的時候,就會出現原型對象包含一個指向另一個原型的指針,例如 dog原型對象 指向了 animal原型對象。

繼續回到現場,咱們作了一些分類,食物下面分了水果分類:數組

// 定義一個 Food 的構造函數
function Food() {
  this.type = "食物";
}
// 定義了 Food 的原型對象的一個方法 getType
Food.prototype.getType = function() {
  return this.type;
};
// 定義一個 Fruit 的構造函數
function Fruit() {
  this.type = "水果";
}
// 將 Fruit 的原型對象指向 Food 的實例
Fruit.prototype = new Food();
// 定義 Fruit 的原型對象的一個方法 getType
Fruit.prototype.getType = function() {
  return this.type;
};

var food1 = new Fruit();
console.log(food1.getType()); // 返回 水果
  • 前半段都是同樣的,直至將 Fruit 的原型對象指向 Food 的實例,因而Fruit原型不只擁有了 Food 實例的所有屬性和方法,也擁有了 Food 實例的原型對象(由於 Food 實例裏面有 prototype 指向Food Prototype
  • 這種粗暴的直接將父對象的實例塞進去子對象的原型裏面的方式,直接促成了Fruit 繼承 Food。

我最喜歡用《javascript 高級程序設計》第三版的圖來講明,由於他畫的比較詳細並且容易看明白(雖然我也是看了十來遍纔看懂),借用他的例子和圖來解釋咱們的例子:app

能夠看到如今這裏子對象 subtype 的 原型對象是 superType,由於也是直接粗暴的塞進去的。

若是要看完整的他的原型鏈,能夠參看這個圖:less

至關詳細,這裏之因此有 Object 是由於 javascript 裏面一切皆是對象,默認的最頂級的原型就是 Object Prototype(怎麼看這個圖,能夠翻看以前一集介紹原型的內容)函數

下面須要注意一些原型對象的問題和技巧

1.1 肯定原型和實例的關係

沒辦法準確知道是繼承於哪個,只要是在鏈條裏面的,都會被認爲是繼承過來的。學習

console.log(food1 instanceof Fruit) // 返回 true
console.log(food1 instanceof Food) // 返回 true
console.log(food1 instanceof Object) // 返回 true

console.log(Fruit.prototype.isPrototypeOf(food1)) // 返回 true
console.log(Food.prototype.isPrototypeOf(food1)) // 返回 true
console.log(Object.prototype.isPrototypeOf(food1)) // 返回 true
這裏也跟javascript 的原型搜索機制有關係,當訪問一個實例屬性時候,首先會在實例中搜索該屬性,若是沒有找到該屬性,就會繼續搜索實例的原型對象,在經過原型鏈實現繼承的狀況下,搜索過程就會一直沿着原型鏈繼續向上搜索。

相似下圖:

圖片來源於:http://www.cnblogs.com/keepfo...ui

1.2 謹慎定義方法

① 給原型添加方法的代碼必定要放在替換原型的語句以後

正確的例子:

// 定義一個 Food 的構造函數
function Food() {
  this.type = "食物";
}
// 定義了 Food 的原型對象的一個方法 getType
Food.prototype.getType = function() {
  return "food 的 getType 方法";
};
// 定義一個 Fruit 的構造函數
function Fruit() {
  this.type = "水果";
}
// 將 Fruit 的原型對象指向 Food 的實例
Fruit.prototype = new Food();
// 給子類 Fruit 的原型添加一個新方法getSubType
Fruit.prototype.getSubType = function() {
  return "Fruit 的getSubType";
};
// 重寫父類 Food 的方法getType
Food.prototype.getType = function() {
  return false;
};
var food1 = new Fruit();

console.log(food1.getSubType()); // 返回 Fruit 的getSubType
console.log(food1.getType()); // 返回 false
  • 子類 Fruit 重寫父類(超類)的原型對象的方法getType,在調用的時候會覆蓋屌父類 Food的原型對象的getType方法,直接使用子類Fruit的getType
  • 子類 Fruit 添加一個方法到本身的原型對象裏面,也是很正常的,可以被直接使用。

錯誤的例子:

// 定義一個 Food 的構造函數
function Food() {
  this.type = "食物";
}
// 定義了 Food 的原型對象的一個方法 getType
Food.prototype.getType = function() {
  return "food 的 getType 方法";
};
// 定義一個 Fruit 的構造函數
function Fruit() {
  this.type = "水果";
}
// 給子類 Fruit 的原型添加一個新方法getSubType
Fruit.prototype.getSubType = function() {
  return "Fruit 的getSubType";
};
// 重寫父類 Food 的方法getType
Food.prototype.getType = function() {
  return false;
};
// 將 Fruit 的原型對象指向 Food 的實例
Fruit.prototype = new Food();

var food1 = new Fruit();

console.log(food1.getSubType()); // 拋出 error 異常
console.log(food1.getType()); // 返回 false
  • food1.getSubType() 直接拋出異常,提示說方法找不到或者未定義
主要就是由於子原型對象被替換的時候會被徹底覆蓋。

1.3 在經過原型鏈實現繼承時,不能使用對象字面量方法建立原型

主要是由於對象字面量方法會重寫原型鏈,這個原理在以前章節說過,這裏只是再次提醒。

// 省略。。。
Fruit.prototype = new Food();

Fruit.prototype = { // 被重寫了原型鏈,就不屬於原來的原型鏈範圍了。
//  xxxxxxx
}
// 省略。。。

1.4 原型鏈的問題

  1. 原型鏈最大的問題是來自包含引用類型值的原型,這種類型值的原型屬性會被全部實例共享,致使沒辦法很好隔離,因此以前也是使用構造函數和原型模式組合使用來解決這個問題,但當時沒有觸及真正的繼承。
  2. 原型鏈另一個問題是,在建立子類型的實例時,不能向超類型的構造函數中傳遞參數,或者說,是沒辦法在不影響全部對象實例狀況下,給超類型的構造函數傳遞參數。
基於以上2個問題,致使了實際環境中,不多會單獨使用原型鏈,會結合其餘方式來使用原型鏈,畢竟 javascript 裏,全部的繼承其實也是以原型鏈爲基礎的。

2、繼承-借用構造函數、僞造對象、經典繼承


圖片來自:https://www.tvmao.com/drama/K...

鑑於以前原型鏈的問題兩大問題,因此機智的工程師想出來利用構造函數來搭配使用,這個技術就叫作借用構造函數 constructor stealing(很 low 有沒有!),有時候叫僞造對象,或者叫經典繼承(逼格瞬間飆升到徹底看不懂,但以爲很厲害,有木有!)

核心思想是在子類型構造函數的內部調用超類型改造函數。

單純使用原型鏈繼承的時候:

function Food() {
  this.colors = ["red", "blue"];
}

function Fruit() {}

Fruit.prototype = new Food();

var food1 = new Fruit();
var food2 = new Fruit();
console.log(food1.colors); // 返回 [ 'red', 'blue' ]
console.log(food2.colors); // 返回 [ 'red', 'blue' ]
food1.colors.push("yellow");
console.log(food1.colors); // 返回 [ 'red', 'blue', 'yellow' ]
console.log(food2.colors); // 返回 [ 'red', 'blue', 'yellow' ]

使用借用構造函數模式繼承的時候:

function Food() {
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this); // call 能夠改變函數的this對象的指向
}

var food1 = new Fruit();
console.log(food1.colors); // 返回 [ 'red', 'blue' ]

food1.colors.push("yellow");
console.log(food1.colors); // 返回 [ 'red', 'blue', 'yellow' ]

var food2 = new Fruit();
console.log(food2.colors); // 返回 [ 'red', 'blue' ]

能夠看到大相徑庭的兩種效果,後者的實例的數組(引用類型的數據)並無跟隨其餘實例變化而變化,是互相獨立的。

爲何能夠這樣呢?

  • 利用了函數的執行環境上下文,這裏的「繼承」的目的只是爲了可以使用超類的屬性和方法(不算是真正的繼承),因此直接將超類的構造函數放到子類的構造函數裏面執行,從而將他們進行合體。
  • 利用了 call(或者 apply 或者 bind 這種函數)改變了構造函數的 this 指向,才得以實現上面說到的將不一樣的構造函數放到同一個執行環境中執行。

2.1 傳參

下面兩個例子分別說明了,這種繼承方式能夠傳參的,而且傳參以後也是能夠重寫超類的屬性的。

例子1:

function Food(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this, "蘋果"); // call 能夠改變函數的this對象的指向
}

var food1 = new Fruit();
console.log(food1.name); // 返回 蘋果

例子2:

function Food(name) { // 參數
  this.name = name;
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this, "蘋果"); // call 能夠改變函數的this對象的指向,加上了傳參
  this.place = "非洲"; // 添加屬性
  this.name = "香蕉"; // 重寫超類屬性
}

var food1 = new Fruit();
console.log(food1.name); // 返回 蘋果
console.log(food1.place); // 返回 非洲

2.2 這種方式的問題


圖片來自:https://www.youtube.com/watch...

正如以前所說,這種不是真正的繼承,只是想子類和父類進行了強行合體罷了,這種合體方式可以知足通常繼承的要求,可是帶了其餘問題:

  • 沒辦法使用超類的原型對象裏面定義的方法。
function Food() {
  this.colors = ["red", "blue"];
}
Food.prototype.getType = function () {
  console.log("我是 food 的getType");
}
function Fruit() {
  Food.call(this); // call 能夠改變函數的this對象的指向
}

var food1 = new Fruit();
console.log(food1.getType()); // 拋出異常,沒有這個 function
  • 由於子類和超類都是構造函數,那麼就會有以前說的,構造函數在 new 的時候,裏面的方法(函數)會重複建立 function 實例, 致使資源浪費。
function Food() {
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this); // call 能夠改變函數的this對象的指向
  this.getType = function() {
    console.log("我是 food 的getType");
  };
}

var food1 = new Fruit();
var food2 = new Fruit();

console.log(food1.getType == food2.getType); // 返回 false
鑑於這種問題,在小規模程序設計裏面還好,可是一旦規模稍微變得複雜以後,就無法控制代碼了,那咱們機智的工程師們還要繼續想一想辦法。

參考內容

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

原文轉載:https://www.godblessyuan.com/...

相關文章
相關標籤/搜索