iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0

了解物件導向程式設計的基本理論、其與 JavaScript (幾乎所有東西都是物件) 之間的關係、應如何寫出建構子與物件實體。

大綱

  • JavaScript 繼承機制的設計理念
  • JavaScript 物件導向的特徵
  • Constructor function 的問題與原型 (Prototype)
  • 物件繼承 - 原型鍊 (Prototype Chain)
  • ES6 之後的物件導向語法
  • 回顧

JavaScript 繼承機制的設計理念

從 JavaScript 的誕生開始說起:

一、從古代說起

在 1994 年有家公司叫 Netscape (網景) 發佈了 Navigator 瀏覽器,但是,這個版本的瀏覽器只能用來瀏覽,不具備跟使用者互動的能力。因此,Netscape 很快了解到 Web 需要變得更動態才行 (具備與使用者互動的能力),急需一種指令搞語言,接著在 1995 年 Netscape 聘用了 Brendan Eich,就在 5 月花了 10 天開發出了這個原型。在開發當時稱為 LiveScript,之後為了搭上 Java 語言的名氣,而改為 JavaScript。

1994 年正是物件導向程式最興盛的時期,C++ 是當時最流行的語言,而 Java 語言的 1.0 版本也即將於第二年推出。 Brendan Eich 很顯然地受到了影響,在 JavaScript 裡面所有的的資料類型都是物件,這一點與 Java 非常接近。接著,很快地他就遇到了一個難題,到底要不要設計繼承機制呢 !?

二、Brendan Eich 的選擇:

如果真的是一種很簡易的指令搞語言,其實不需要有繼承機制。但是,在 JavaScript 裡面都是物件,就必須需要一種機制,將所有物件聯繫起來。最終,Brendan Eich 還是設計了繼承。

但是,這時侯他不打算引入 class (類別) 的概念,因為一但有了class (類別),JavaScript 就是一種完整的物件導向程式語言了,這又有點太過於正式,而且也會增加了初學者的入門難度。

他考慮到,C++ 和 Java 語言都是使用 new 命令,來生成 instance (實體 / 實例)。

C++ 的寫法是:

ClassName *object = new ClassName(param);

Java 的寫法是:

Foo foo = new Foo();

最後,他就把 new 命令引入到 JavaScript,用來從原型物件生成一個實體物件。但是,JavaScript 沒有 class (類別),哪要怎樣表示原型對象呢 !? 這時,他想到 C++ 和 Java 使用 new 命令時,都會使用 class (類別) 的 constructor function (構造函式)。他就做了一個簡化的設計,在 JavaScript 語言中,new 命令後面跟的不是 class (類別),而是 constructor function (構造函式)。

JavaScript 物件導向的特徵

從 ES6 起最大的改變就是物件導向語法。在 ES6 中導入的 class 語法,使得整個程式外觀改變。為了配合這本書當時的時空背景,這次會以 ES6 之前的語法來做說明與介紹,後半段再來介紹 ES6 中導入的 class 語法。

何謂物件導向程式 (object-oriented programming, OOP)?

JavaScript 中使用一種名為 constructor function 的函式來定義物件和它們的特色,透過 constructor function 可以根據你的需要用更有效率的方式來產生許多物件,而這些物件都已經包含你所定義好的屬性和方法。

// 未來有空再改寫

原型基礎物件導向

If you don’t understand prototypes, you don’t understand JavaScript.
如果你沒搞懂原型,你不算真的懂 JavaScript。

不是「類別」而是「原型」

JavaScript 是以原型為基礎的物件導向設計,一直到 ES6 標準制定後仍沒變動過。在 ES6 新增的 class 定義方式,也只是原型物件導向語法的語法糖,骨子裡還是原型,並不是真正的以 class 為基礎的物件導向設計。

原型:以某物件為基礎的物件,JavaScript 中使用原型 (取代類別) 建立新的物件,此種性質的物件導向就稱為以原型為基礎 (Prototype-based) 的物件導向。

作為實體工廠的建構器

正式開始說明如何使用 JavaScript constructor function 之前,先來看看,如果要設計衣服是如何進行的呢 !?首先,會有一個衣服的設計圖,在這裡定義了顏色及尺寸,接著就會根據設計圖製作出實際的衣服,每件衣服都是同樣的款式,但會擁有自己的顏色及尺寸,每件衣服都會別上一個名牌。

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4d048afb-6273-41c4-a26a-65816773a37d/colthes_oop.png

根據上圖的範例,如果要使用 JavaScript 告訴電腦衣服的設計圖呢 !?

第一步,使用 Constructor 初始化

function Clothes(color, size, country) {
  this.color = color;
  this.size = size;
  this.country = country;

  this.whichCountry = function () {
    console.log('Made in ' + this.country);
  }
}

這裡有幾點妳我必須知道:

  • constructor function:產生 instance 時用以初始化處理的特殊方法 or function。講白話點就是製作大量相似的物件。
  • 為了要區別 constructor function 與一般函式,constructor function 命名時通常會以大寫字母作為開頭。
  • 會使用 new 這個保留字作為搭配。

第二步,呼叫 new 運算子建立物件

const c1 = new Clothes('red', 'XL', 'Taiwan');

const c2 = new Clothes('blue', 'L', 'Japan');

const c3 = new Clothes('black', 'M', 'Korea');

console.log(c1);

console.log(c2);

console.log(c3);

語法:new運算子

var 變數名稱 = new 物件名稱([參數,...])

new :創造一個空的物件。/ 告訴記憶體,你要開一個空間給我,我要創造一個新的物件。

第三步,結論

整個上述的程式整理如下:

function Clothes(color, size, country) {
  this.color = color;
  this.size = size;
  this.country = country;

  this.whichCountry = function () {
    console.log('Made in ' + this.country);
  }
}

const c1 = new Clothes('red', 'XL', 'Taiwan');

const c2 = new Clothes('blue', 'L', 'Japan');

const c3 = new Clothes('black', 'M', 'Korea');

console.log(c1);

console.log(c2);

console.log(c3);

問題:

  1. 這裡的this指的是什麼 !?
  2. 不使用new運算子,會發生什麼事 !?

根據上述的程式碼,對照下圖妳我可以了解原型與物件的區別,以及new=等語法的使用:

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/473e280a-71ee-4fa0-83cd-1693a4476b4d/.png

Constructor function 的問題與原型 (Prototype)

在上面的內容提到,若要定義 instance 共用方法,哪就需要在 constructor function 中定義方法,但是,在 constructor function 新增方法會隨著方法數量增多而浪費記憶體的問題。

什麼意思呢 !? 來看範例。

function Clothes(color, size, country) {
  this.color = color;
  this.size = size;
  this.country = country;

  this.whichCountry = function () {
    console.log('Made in ' + this.country);
  }
}

const c1 = new Clothes('red', 'XL', 'Taiwan');

const c2 = new Clothes('blue', 'L', 'Japan');

console.log(c1.whichCountry === c2.whichCountry)

根據上述的程式碼,對照下圖妳我可以了解 Constructor function 問題的所在:

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fba4cb8e-13c1-44f2-b027-218df8a3b122/constructor_.png


繼續上述未講完的故事:

三、Constructor function 的問題

由於上述的問題產生,透過 constructor function 生成的 instance,有一個缺點,就是無法共享方法。

四、prototype 屬性的引入

考慮到了這一點,Brendan Eich 決定為 constructor function 設置一個 prototype 屬性。這個 prototype 是一個物件,所有 instance 需要共享的方法,就放入這個物件裡面 ; 哪些不需要共享的屬性,就放入 constructor function 裡。

因此,一但 instance 被創建之後,將會自動引用 prototype 物件的方法。也就是說,instance 物件的屬性和方法分成兩種,一種是本身自己的,另一種就是引用的。

接著,上述的問題,現在使用 prototype 屬性進行改寫,請看以下程式碼。

function Clothes(color, size, country) {
  this.color = color
  this.size = size
  this.country = country
}

Clothes.prototype.whichCountry = function () {
  console.log('Made in ' + this.country)
}

const c1 = new Clothes('red', 'XL', 'Taiwan')

const c2 = new Clothes('blue', 'L', 'Japan')

console.log(c1.whichCountry === c2.whichCountry)

現在,whichCountry這個方法是放在 prototype 物件裡面,是這兩個 c1 及 c2 instance 共享的。只要修改了 prototype 物件的whichCountry方法,就會同時影響到所產生的這兩個 instance。

五、總結

由於所有的 instance 物件共享者同一個 prototype 物件,哪也就是說,prototype 物件就好像是 instance 物件的原型,而 instance 物件就好像繼承了 prototype 物件一樣。

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b3b2e2b4-b8f3-4c7e-ba8e-c55ef6542f2b/prototype.png

物件繼承 - 原型鍊 (Prototype Chain)

物件導向程式語言很重要的一個概念是就是繼承,所謂繼承就是保有原本物件的功能並用以定義新類別的功能。

直接來看範例程式:

function Clothes(color, size, country) {
  this.color = color
  this.size = size
  this.country = country
}

Clothes.prototype.whichCountry = function () {
  console.log('Made in ' + this.country)
}

const c1 = new Clothes('red', 'XL', 'Taiwan')
c1.whichCountry()

跟據上述的程式碼,要思考的方向有幾點。

  • Clothes.prototype增加了whichCountry這個 function。
  • 透過new運算子新增了一個 instance,並且將值賦予給 c1 這個變數。
  • 接著可以透過c1.whichCountry()呼叫到whichCountry這個 function。
  • 問題來了,要思考的點是,為何c1.whichCountry()可以正常執行 !? 是不是就代表c1Clothes.prototype透過某種方式,將這兩者連接起來。不然的話,JavaScript 的引擎怎麼會知道whichCountry()這個方法要來Clothes.prototype這裡找。
  • 所以在 JavaScript 裡有內建一個屬性叫**__proto__。**這個屬性就是暗示說如果在 c1 身上找不到 whichCountry 的話,哪應該就要去哪裡找。這個概念其實會跟上去我講到的scope chain有點像。
  • 哪我們就先把這個給印出來,看會是什麼console.log(c1.__proto__)。其實就是一個物件,內容長這樣。
  • 其實他就是console.log(c1.__proto__ === Clothes.prototype),會得到 true。
  • 打開右邊視窗的畫面,要開始輸入另外一個視窗的內容。
    • 當呼叫c1.whichCountry()的時候,運作流程:
      1. 第一個先去找 c1 身上有沒有 whichCountry ? // 在這個 case 是沒有的。
      2. 第二個去找c1.__proto__身上有沒有 whichCountry ?
    • 其實呢 ? 這個c1.__proto__ = Clothes.prototype,這是為什麼呢 !? 這個就是new幫我們做的事情,後面有時間的話,再來講到。
  • 所以,在這個範例,他就去找Clothes.prototype有沒有這個 whichCountry,哪結果是有,哪就是說他找到了。這裡的this指的就是這個 c1。

哪問題來了,如果還是找不到呢 !?

  • 打開右邊視窗的畫面,要開始輸入另外一個視窗的內容。
    • 第三個再去找上一層c1.__proto__.__proto__身上有沒有 whichCountry ?
    • 其實這就是console.log(c1.__proto__.__proto__ === Object.prototype),會得到 true。

哪問題又來了,根據上面的程序,如果還是找不到呢 !?

  • 打開右邊視窗的畫面,要開始輸入另外一個視窗的內容。
    • 第四個,就再去找上一層c1.__proto__.__proto__.__proto__身上有沒有 whichCountry ?
    • 印出來console.log(c1.__proto__.__proto__.__proto__)會得到 null。

ES6 之後的物件導向語法

在 ES6 新增了 class 指令,對定義 JavaScript 的類別也變得更簡單及直覺,請看以下範例:

class Clothes {
  constructor(color, size, country) {
    this.color = color
    this.size = size
    this.country = country
  }

  whichCountry() {
    console.log('Made in ' + this.country)
  }
}

const c1 = new Clothes('red', 'XL', 'Taiwan')
c1.whichCountry()

class 指令的語法如下。

class 類別名稱{
	...建構子定義...
	...屬性定義...
	...方法定義...
}

物件導向的繼承 - Inheritance

class Clothes {
  constructor(color, size, country) {
    this.color = color
    this.size = size
    this.country = country
  }

  whichCountry() {
    console.log('Made in ' + this.country)
  }
}

回顧

  1. 透過這個章節了解到,已 ES6 前跟後對於 JavaScript 在物件導向語法的不同。
  2. 了解 constructor function 與 new 的用法。
  3. 了解原型鍊 (Prototype Chain) 的概念。

上一篇
Day 25 | 模組系統與套件管理器
下一篇
Day 27 | Variables: Scopes, Environments, and Closures
系列文
一步一腳印,我的前端工程師修煉30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言