前端中階:JS令人搞不懂的地方-物件導向

什麼是物件導向?

在前篇 closure 的最終範例 前端中階:JS令人搞不懂的地方-Closure(閉包)

實際上就很接近物件導向的概念了

以本來的改寫前的範例來說,因為任何人都可以改 money 的值。而在物件導向的世界,呼叫 function 的方式則不太一樣,用起來就像是個物件的形式。很多時候,我們在用的時候就像是物件導向。

myWallet.add() 意思就像是對 myWallet 這個物件做一些操作。

把很多的東西,都做成一個一個的物件,這樣就可以不需要一直 Call function,看起來也模組化一些,資訊也可以隱藏起來。

物件導向的基礎範例

在 ES6 之後就有了 class 這個語法可以使用。這是一種語法糖,實際上的 JavaScript 本身並沒有 class 可以使用,但這邊就先以 class 來說明物件導向。

class 後面要接名稱,名稱務必大寫。

定義好的 class 就只是一個設計圖。還必須要把它實體才可以。就需要通過 new 指令

通過 new 指令,就像是把一個設計圖實現,真的出現一隻狗的那種感覺,就可以稱之為 instance。

然後就可以呼叫函式

當然除此之外,還可以新增更多的 Function

this 在 class 的用途是誰呼叫它就指向呼叫它的,在這邊是 d.setName('abc'); d 通過 setName 呼叫 this,所以這個 this 就是 d

setName 就會被稱為 setter,當然就還有 getter

有設定就會有取得,這是很常見的模式,一般而言,不會想直接去操作 class 內部的值,通常是直接操作 getter & setter 會比較方便。

一般我們在建立狗的時候,都會想給它一個名字。所以就可以通過 instance 的時候傳參數的方式,來設立。

那至於 class 要怎麼實作,就是有一個建構子 constructor,可以用來接收參數。在 call new Dog 的時候,就等於在呼叫 constructor,所以就可以傳入變數。constructor 的目的就是讓 instance 初始化,所以建立的時候,就會呼叫 constructor。而 this 則是指向那個 instance。

當然也可以另外建立一個新的 instance。

ES5 的物件導向

接下來就是要看 ES5 的情況之下,物件導向長什麼樣子,利用前面的例子反推回去,會比較清楚。

ES5 寫法:

沒有 new 也可以跑。每次弄一個新賦值都會回傳一個物件,所以就可以測試一下。

會發現個同樣的 function 卻不是一樣的東西,代表儲存了兩次。不過一般而言,在這種情況下我們會希望可以只使用單一個 function 去跑不同的東西即可。

因為如果按照這個模式來說,如果有一千隻狗,就等於有一千個 sayHello() 的 function 了,每個都是做一樣的事情,這種情況下我們只需要一個一樣的 function 就好了。

以 class 中 this 來說,大家都是跑一樣的東西,只不過因為是不同的 instance ,所以這個 this 指向的地方不一樣。但都是同一個 function。

而在 ES5 就有提供一個很類似的機制,因為希望可以保持一樣的語法:
var d = new Dog('abc'); ,然後利用 function 實作。

這邊就可以看到 d 就是 Dog 的 instance。

那平常要怎麼知道是 function 還是 constructor,就是傳入的時候有加上 new 就會當作 constructor。

所以不加的話,就會 return undefined 因為並沒有回傳任何東西:

設定的部份完成了,接下來就是實作 function,JavaScript 的機制有個 .prototype 就可以連結 function

接下來就可以測試兩個 function 是不是一樣的:

兩個就會一樣了,因為這兩個都是 prototype 上面的 function,就可以利用這種方式在 JavaScript 上面實作物件導向。

從 prototype 來看「原型鍊」

先定義一下 .__proto__.prototype 的差異。

.prototype :用來實現基於原型的繼承與屬性的共享。ex: 用來指定屬性或 function

.__proto__ :構成原型鏈,同樣用於實現基於原型的繼承。ex: 繼承資料

來源:知乎

繼續沿用範例講解

以這例子來說,JavaScript 一定有某些機制,才可以把 d.prototype.sayHello() 連結在一起。

在 JavaScript 裡面有個屬性是 .__proto__ 這個屬性是暗示說,你在 d 身上找不到,就去找 d.__proto__

這個就很類似於之前講到的 scope chain。

所以就可以印出這個部份:

所以其實就是 Dog.prototype

兩者印出來的資料是一樣的。

所以 class 只是形式比較好看而已,實際上的底層還是一樣是 .prototype

實際運作如下:

當呼叫 d.sayHello() 的時候,

所以印出試試看

接著來測試看看

這邊測試看看幫 Object 還有 Dog 之間的比較,彼此針對 prototype 加上 function

會發現呼叫到 dog 的 function 理由很簡單,因為會從 d 開始往上找 d.__proto__ ,再找到 d.__proto__.__proto__ 最後找到頂 d.__proto__.__proto__.__proto__ 這就會形成一個 prototype chain 就稱作為原型鏈,這就類似之前說的 scope chain。在 JavaScript 裡面有很多類似的 chain 機制。

Dog 的 .__proto__ 會是什麼?

會是一個 Function 的 prototype 這很合理,因為本來就是 function 的一種。

因為 d 是 Dog 的原型,d 這個 instance 可以通過 d.__proto__Dog.prototype 連接在一起。所以就可以利用 .__proto__ 去找到要找到的東西。

toString.call()

講解 前端中階:JS令人搞不懂的地方-變數 寫到的部分。

這不能寫成

這樣會把內容轉換成 string 而已,因為當呼叫 a.toString() 時,實際上 a 並沒有 a.toString() 這個方法。
這個字串其實本身會有自己的 prototype chain。

這樣用的效果是當 a 沒有,就去找 a 對應的屬性的 .__proto__
的方法,所以就會找到 String.prototype 上所有的方法。因為是通過 prototype chain 才可以取得針對 String 的方法。所以即使名字一樣,有會一位這種 prototype 的原因而找到不一樣的同名方法。

從這邊就可以證明,就是通過 prototype chain 才可以呼叫得到的。

所以要呼叫 Object 裡面的 toString 就必須要想辦法避開這個 prototype chain 才可以呼叫到,這就好像是不同的作用域之間,有著同名變數、函式一樣。呼叫同名函式的時候,永遠就會先呼叫到最接近自己的作用域的函式。再上層的就無法呼叫,因為被擋住了。

不過也可以反其道而行,直接針對 String 增加一個函式利用,

所以我們可以在 String 的 prototype 添加一個方法,這樣子所有的 String 都可以使用這個方法了。這種方式針對 Array 也可以。

所以就可以使用 .__proto__.prototype 來做很多處理。

總結就是可以利用 .prototype 這個特性來新增變數、函式。
而通過 .__proto__ 就可以來找查變數、函式、屬性、方法等資源。

new 背後做了些什麼事情

在理解 new 之前先理解一個知識。

會發現 this 有非常龐大的值

這不是這篇的重點,知道 this 有這麼多東西即可

除此之外, function 還有一種呼叫方式叫做 .call() 我們使用 test 並且帶入值 test.call('123')

就會發現印出的東西改變了。

意思就是說,使用 .call() 的時候,傳入的東西就會變成 this 的值。

.call() MDN 說明 ,寫的還滿困難的。

我自己的話是解釋成 .call() 第一個參數是 function 或值 ,其他就是 function 傳入的值。
下面的說法也很類似,只是他把第一個當成第零個,實際上就像是有的程式語言的 array 是從 0 還是從 1 開始一樣的問題,知道意思就好。

任何一個 function.call() 的狀況下,我們可以把整個參數陣列( arguments )向右平移,而第零個參數則是告訴函數我們想使用的 this
by 原型函數最實用的三個方法

通過 .call() 第一個參數就是 this 指向的對象的原理,就可以試著手動建立一個 new 的模型。

以上面的範例來說,這邊要模擬 new 做的事情。所以另外設立一個 newDog 來模擬:

  1. 會先建立一個空物。
  2. 呼叫 constructor 。把 Dog 利用 .call() 帶入空物件,第一個參數放入 空物件 (obj) 從這邊就可以知道 this 就是指向 obj,.call() 第二個之後的參數就是原本要帶入的值。

試著印出來,就會發現印出了一個物件,是一個把資料都放好的物件。

接下來就要指定 Function 的部份。

3. 建立物件的原型鏈,針對 obj 的 .__proto__ 等同於 Dog 的 .prototype 就可以完成關聯。

4. 把完成的物件回傳,就是一個玩整個機制。

並且使用 b.sayHello() 就會發現可以印出資料。

小結:

在使用 new 的時候的流程就如同上方呈現的:

  1. 建立一個空物件
  2. 連結 constructor ,這樣才可以初始化建構子的內容。
  3. 建立物件的原型鏈,少了這步驟就沒有 function 可以使用。
  4. 回傳這個完成的物件。

物件導向的繼承:Inheritance

繼承的部份,就像是繼承自己爸媽的 DNA

雖然 BlackDog 沒有 constructor 但在這邊依然可以印出,主要是因為繼承了 Dog 的 constructor。當然也可以直接使用 Dog 的方法。

繼承在這邊的用途就是,當需要用到一些屬性的時候,就可以直接從別的物件導向那邊繼承,不用都要自己寫。

狗本身就有名字有動作,有自己的屬性跟方法。而如果要另外一種狗,例如黑狗,黑狗也會有同樣的屬性跟方法,只是可能有些微的差異,所以就可以通過繼承直接使用狗的屬性跟方法,就不用自己重新寫過。

假如我們希望可以讓這隻 BlackDog 被建立的時候,就打招呼。那也可以直接寫入 constructor。

會發現產生錯誤,告訴我們在呼叫 this 之前必須要 call super。

否則照原本的模式,就只會初始化 BlackDog 的 constructor,就沒有了繼承的作用了。

所以 super() 等於呼叫了上一層繼承層的 constructor,那既然已經繼承了上一層的 constructor 就必須要也要接收,所以 constructor 就要接收上一層的 name。

這樣子才可以連上一層的 Dog 的 constructor 一併初始化,之後並接收這些初始化的值。

繼承之後在 constructor 一定要使用 super() 跟變數去接收這些初始化之後的資料。

繼承的好處就是可以先把一些基本的東西寫好,然後細微的部份再繼承之後做細微的修改。

總結:

終於把物件導向的部份學習完成了。但我似乎想的有些複雜,才導致我去查了很多資料。

不過也實在是因為物件導向的東西比較底層,所以在學習上本身就會有一定的難度。

尤其是那些 .__proto__ 以及 .prototype 使用 console.log() 出來之後更是讓我倍感複雜。所以這部份為了可以更好的理解,我就有去看了一些文章。

果然如此,原型鏈真的要很複雜也很複雜,因為還有更多更細節的部份,也一樣會需要看到 ECMA 的說明書才可以理解其定義。

這邊會覺得有定義還是重要,先給個定義之後,再去學習可能會好一些。在中途可能因為不是很清楚定義到底是什麼,所以中間就越來越混亂了。

於是我就找了大量的資料,像是找到了知乎的一篇,寫的好詳細阿!各種五花八門的寫法,但多瀏覽幾次之後就會發現實際上沒這麼困難。

比較簡單的是有沒有 new 過 new 過得就變成一個實體,實體的 .__proto__ 就會指向其 new 之前的 function 的 .prototype

function.prototype.__proto__ 就會等於 Object.prototype 等等的部份。

另一個我覺得也滿重要的是 functionX 的 .__proto__ 每一層各自有什麼,通過理解到底有什麼會讓我把整體狀況弄得更加清楚:

functionX.__proto__ 等於 Function.prototype

functionX.__proto__.__proto__ 等於 Object.prototype

functionX.__proto__.__proto__.__proto__ 等於 null

這樣研究下來之後,就不會覺得有些什麼地方沒這麼清楚。當然我也知道因為那張圖的關係,我會有許多的東西並沒有搞得很懂。這部份也許之後會在來學習了。

目前已經進化成軟體工程師,這個部落格目的在分享一些自己的學習心路歷程以及作為技術部落格。技能是 React + Express.js。在此之前是一名化工工程師,因為對電腦科技一直存在熱情,參加了 huli 實驗計畫是三期的學生,所以開設了這個部落格紀錄學習路程,而在 2020/06/15 正式轉職成功後,依然不斷精進。

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store