iT邦幫忙

2021 iThome 鐵人賽

DAY 11
1
Modern Web

Javascript 從寫對到寫好系列 第 11

Day 11 - OOP 初探 (1) - Closures 與繼承鏈

前言

在學習 FP 的過程中,會看到 FP 常常被拿來跟 OOP 做比較,那 OOP 究竟是什麼呢?它們是對立關係只能選一種用嗎?誰比較有優勢呢?

今天會先簡單介紹 OOP 大概是什麼,以及幾個支持 OOP 的重要支柱們。

OOP(Object Oriented Programming)

中文翻作「物件導向程式設計」,簡單來說就是以 物件(object) 為主體的程式設計風格。

這句話是否似曾相識呢?

沒錯,因為前三天提到的 FP(Functional Programming),就是以 函式(function) 為主體的程式設計風格。

OOP 與 FP 的差異

基於讀者可能已經看完前面三天,對於 FP 這種程式設計風格有基礎概念了,所以我想讓讀者在開始學習 OOP 之前,有正確的學習觀念,以免被一些不由自主形成的對立影響了。

FP 是一種程式設計風格,OOP 則是另一種程式設計風格。

就好像 RPG 遊戲裡面玩騎士的角色擅長近距離戰鬥,但肯定也有不擅長的吧!所以我們會有其它角色像是弓箭手,擅長遠距離戰鬥。

FP 跟 OOP 就像是上述的關係,FP 擅長處理「流程」相關的問題,那遇到「非流程」相關的問題該怎麼辦?就可以考慮用其它像是 OOP 這種,擅長解決不同問題的程式設計風格。

我該選 OOP 還是 FP?

何不全都要?

RPG 遊戲為了角色平衡,一次就只能選一種角色,但我們人類的潛力是無窮的,你可以是騎士在最前方衝鋒陷陣,也可以同時配一把弓,在適合遠距時攻擊。

文藝復興時期的達文西,在繪畫、音樂、建築、數學、幾何學、解剖學、生理學、動物學、植物學、天文學、氣象學、地質學、地理學、物理學、光學、力學、發明、土木工程等領域都有顯著的成就。被稱為「博學者」(polymath)。

有人會擔心,是不是整個團隊都一定要用 OOP 或都用 FP,不然會不會寫到一半風格有落差開始打架?

其實不會的,因為一個網頁包含了許多功能,每一個功能都是要解決使用者的問題,可以根據每個問題的特性,去思考要使用 OOP 或 FP,甚至其它,這樣才不會發生讓弓箭手近距離跟狂戰士互 A 的慘況。

結論是通通學起來!

OOP 兩大支柱

既然已經確定 OOP 就是可以成為自己的助力,那就開始來學習吧!

OOP 是以 object 為主體的思考,所以我們要先來學習,Javascript 跟物件相關的兩個重點:Closures、Prototypal Inheritance

Closures

中文翻作「閉包」,它是 JavaScript 的一個資訊隱藏機制。

資訊隱藏

先從我們常見的物件當範例吧:

const person = {
    name: 'Joey',
    age: 20
};

有夠普通的物件,但如果今天我們是個員工管理系統,可能需要紀錄員工的薪水:

const person = {
    name: 'Joey',
    age: 20,
    salary: 40000,
};

阿捏母湯啊,我只要不小心印出 person.salary 就看光光了,甚至不小心 person.salary *= 3 就晉升中產階級了。

即便不是這麼刻意,也很容易因為 Object.entries(person) 這種不經意的列舉,就把比較隱私的資料暴露出來。

這邊所謂的「隱私」資料其實不一定是「不能被看到」,因為要是不能看乾脆就不要放就好啦!這個「隱私」比較是「避免無意中被修改」。

私有特性(private properties)

為了避免無意中被修改,我們需要讓某些 property 是 private,但 Javascript 並沒有提供這樣的關鍵字,以往都只能用 coding 的慣例來約束這件事,比如在私有特性的名稱前後綴一個底線(_),比如 _salary,告訴其他開發者,這是一個不能直接被檢視、編輯的屬性。

但很明顯這種方式,防君子不防豬隊友,如果要硬改絕對是可以,因此它來了 - Closures。

搭配函式實現 Closures

要使用 closures 的機制,必須透過 function 來產生 object,先把上面的範例改成用 function 來產生:

const createPerson = (name, age) => {
    const salary = 40000;
    const getSalary = () => salary;
    
    return {
        name,
        age,
        getSalary
    };
}
const person = createPerson('Joey', 20);

console.log(person.salary);
console.log(person.getSalary());

執行結果

undefined
40000

有發現這裡發生很神奇的事情嗎?乍看之下 salary 這個變數只有在 createPerson 函式的 scope 裡面,而且又沒有被回傳,感覺在函式外應該無法取得才對。。。

Closures 的形成方式

在函式中可以定義另一個函式時,如果內部的函式參照了外部的函式的變數,一旦外部的函式被執行,則產生閉包。

以上面的範例來說,會做以下判斷:

  1. createPerson 裡面的 salary 沒有被存取
  2. 但是 salary 有出現在 getSalary 裡面
  3. getSalary 被回傳到外面去了
  4. 此時 closures 機制啟動,把 salary 放到封閉的變數環境內
  5. 若呼叫 .getSalary(),就會到變數環境裡面把 salary 抓出來

總結一句話就會是:內部函式變數有參照外部變數,就會產生 closures

Closures 機制會把資料儲存在他們封閉起來的變數環境中,不提供對這些變數的直接存取。唯一的辦法是,要在函式內明確提供存取它的方式。

Prototypal Inheritance

中文是「原型繼承」。這東西大家應該就比較有概念了,因為這東西可能從學習 Javascript 的第一天,就已經在用繼承的東西了。

陣列的身世之謎

比如說:

const arr = ['Jack', 'Allen', 'Alice'];

arr.forEach(item => console.log(item));

看起來很熟悉對吧!但你有想過嗎?我們使用點運算子(.)都是用在物件取得 property 對吧(比如說 person.name),那這明明是個陣列,為什麼可以用 arr.forEach

這邊有個驚人的事實要告訴你:

const arr = ['Jack', 'Allen', 'Alice'];
console.log(typeof arr);

執行結果

"object"

是的各位觀眾,陣列在 Javascript 裡面,被分類在 object 裡面

其實前幾天在講陣列的時候就隱約有提過,陣列其實就是 key 比較特別的物件,因為陣列只能用數字當 key。

現在你知道有繼承的概念了,就不難猜到,陣列就是繼承物件產生的,所以陣列可以使用點運算子(.),也就一點都不奇怪了。

內建函式的源頭?

即便知道陣列是其中一種物件,但我又沒有在這個物件宣告 forEach 這個 property,為什麼還是可以直接用?

那是因為雖然我們宣告陣列都偷懶使用中括號:

const arr = ['Jack', 'Allen', 'Alice'];

但其實它在背後是這樣跑的:

const arr = new Array('Jack', 'Allen', 'Alice');

各位,「繼承」的關鍵語法登場了你有看到嗎?

繼承的關鍵語法

隆重介紹!「繼承」的關鍵語法就是 new

new 的功能是「產生物件」,但要產生什麼物件呢?要在 new 後面放一個 constructor(建構子),定義要產生什麼物件,可以想像建構子就是個工廠,專門量產物件用的,而建構子必須要是一個 function,我們拿上面 closures 提到的例子來改一下:

const Person = function (name, age) {
    const salary = 40000;
    this.name = name;
    this.age = age;
    this.getSalary = () => salary;
}
const person = new Person('Joey', 20);

console.log(person.salary);
console.log(person.getSalary());

執行結果

undefined
40000

注意幾個重點:

  • 建構子(constructor)命名盡量使用大寫開頭(方便一眼識別)
  • 建構子函式必須是一般的 function,不可以用箭頭函式
  • this 關鍵字代表「這個物件」

從範例可以看到:

  1. 有一個建構子函式叫做 Person
  2. 我透過在 new 的後面放上 Person,產生了一個 Person 物件
  3. 並且把這個 Person 物件放到 person 變數

透過繼承得到建構子的 property

如果以上你都有看懂,那能否回答我,為什麼 person.getSalary() 可以執行呢?

思考的分界線

沒錯,因為我們在 Person 的這個建構子函式中,有宣告了 getSalary 這個 property,因此 person 才可以直接使用 person.getSalary()

同理可證,上面那個 arr.forEach 的問題,你了解為什麼 arr.forEach() 可以執行了嗎 : )?

繼承鏈

把原型想像成有一條鏈子,鏈子上勾著一個個物件,而每個物件的原型都指向前面的物件(可以透過 .__proto__ 找到上層):

const arr = [];
console.log(arr);
console.log(arr.__proto__); // Array 的原型
console.log(arr.__proto__.__proto__); // Object 的原型
console.log(arr.__proto__.__proto__.__proto__); // null

可以看到 arr 的 property 是一層一層繼承下來的,所以它才會同時有 Array 跟 Object 的原生屬性。

換句話說,原本你可以直接用 arr.forEach,但如果打斷原型鏈,就會突然沒了爸爸的庇護(?):

const arr = [];
console.log(arr.forEach);

arr.__proto__ = null;
console.log(arr.forEach);

執行結果

ƒ forEach() { [native code] }
undefined

所以沒事不要亂斷絕父子關係啊(?),會少了很多援軍啊!

結語

今天開始了 OOP 之旅,但其實也是藉 OOP 的名義,來學習一些跟 object 相關的進階觀念,當然其實是滿複雜的,平常很多 method 我們用得很順,也不曾認真去思考它從何而來。

當然我們不是來學考古的,即便不知道 arr.forEach 怎麼來的又怎樣?但那是因為要進到 OOP 的領域,有一些先備知識需要先掌握,明天才會如魚得水!

真正要上場實戰之前,還是要先蹲一下馬步囉!

我來自於你
來自於天空
來自於宇宙

參考資料

Closures MDN
new operator MDN


上一篇
Day 10 - Functional Programming 初探 (3) - 實戰購物車流程
下一篇
Day 12 - OOP 初探 (2) - Class
系列文
Javascript 從寫對到寫好30

1 則留言

0
TD
iT邦新手 4 級 ‧ 2021-09-28 09:56:02

它是 Javscript 的一個資訊隱藏機制。

"JavaScript" :)

ycchiuuuu iT邦新手 5 級 ‧ 2021-09-29 23:01:50 檢舉

感謝幫忙勘誤!不過我現在才發現我全部都拼 Javascript,s 小寫XD" 等寫完再一次改好了

我要留言

立即登入留言