【JS上課筆記】JavaScript 物件導向:建構式、class、原型鏈


Posted by Annie-Chien on 2022-07-12

這一系列的文章為修習 Udemy 的 The Complete JavaScript Course 2022: From Zero to Expert! 這堂課時所做的筆記。


淺淺地談、不專業地談 . . .

什麼是物件導向(OOP)?

OOP 的全名為 object-oriented Programming,是一種程式設計的方法(programming paradigm),一切都是由物件堆積出來,物件就是它的靈魂。

class 類別

class 就像是設計藍圖,用來作為之後創建物件的模型。
透過 class 製作出來的物件,我們就將它稱之為 class 的實例(instance)。class 本身並非物件!
例如:建築師畫出房子的設計圖(class),接著交由工程團隊根據設計圖建造出真正的房子(instance)。
從 class 建構出 instance 的過程又叫 instantiation(實例化)。

OOP的四大原則

1. abstraction 抽象

class 裡面請省略那些「不知道不會怎樣」的資訊。
例如:買蛋糕時會影響我們是否要購買的因素有價錢、尺寸、口味、造型等,至於蛋糕是誰做的、製作時用了哪些器具這些有的沒的問題 I don't care,所以不要跟我說。

2. encapsulation 封裝

class 裡面的部分 properties & methods 只能在該 class 裡面使用。(是我的,外面的人不能碰!)

3. inheritance 繼承

有兩個長得很像的 class 時,則可以利用「繼承」特性,也就是説其中一個當父母,另一個(小孩)則會「繼承」父母擁有的 properties & methods,同時也能擁有自己的 properties & methods。

4. polymorphism 多型

意思是小孩可以自己覆蓋掉(overwrite)從父母那裡繼承的 properties & methods。
例如:繼承了父母的單眼皮,但長大後自己去割雙眼皮(?是可以這樣亂舉例的嗎)。

JS裡面的OOP

JS裡面有prototype(原型),而所有物件都會繼承於一個 prototype,所以物件可以取用 prototype 裡面的 properties & methods,這被稱作 prototypal inheritance。
例如:map()是 Array.prototype 裡面的 method,因為 Array.prototype 是所有 array 物件的原型,所以所有 array 都可操作 map()。

prototypal inheritance跟Class inheritance有些不同
prototypal inheritance(原型繼承)是物件繼承自 prototype(本身也是物件)。
OOP 裡面的 class inheritance 則是一個 class 繼承自另一個 class。

操作 prototypal inheritance 的三種方法

1. Contructor functions 建構式函式

const Person = function(firstName, birthYear){
    //Instance peoperties
    this.firstName = firstName;
    this.birthYear = birthYear;

    //Bad practice, never do this!
    // this.calcAge = function(){
    //     console.log(2022 - this.birthYear);
    // };
};
const Annie = new Person('Annie', 1997);
console.log(Annie)
  • 其實就是普通的函式,差別在於 constructor function 會用 new 來呼叫
  • 不要在constructor function裡面放 method!
    為什麼?想想今天要是要利用此 constructor function 製作一千萬個物件,那這樣就會複製一千萬個相同的 method 啦,這是一件耗效能的事情。至於那要怎麼做呢?我們下面談!
  • constructor function 的名稱首字母要大寫(不這麼做不會怎麼樣,但大家習慣這麼做)
  • 使用 new 時會發生的四步驟:
    a. 生成一個空物件 {}
    b. 呼叫函式,this 被指定為剛剛生成的那個空物件
    c. 空物件的 .__proto__ 屬性,和 constructor function 的 prototype 屬性產生連結(不太明白沒關係,後面說明)
    d. 函式自動回傳剛剛生成的那物件

constructor function 的功能其實就類似於 OOP 裡面的 class,因此我們也會把使用 new 創建的物件稱作為 instance。若要檢查某物件是否為特定 constructor function 所創建出來的 instance,可以使用 A instanceof B,例如 Annie instanceof Person 就會回傳 true。

所以,要怎麼在 constructor function 裡面增加 method 呢?就這麼寫就對了:

Person.prototype.calcAge = function(){
    console.log(2022 - this.birthYear);
};

什麼什麼!.prototype 是哪來的新魔法?
重新整理一下剛剛的程式碼作為說明範例

const Person = function(firstName, birthYear){
    this.firstName = firstName;
    this.birthYear = birthYear; 
};
Person.prototype.calcAge = function(){
    console.log(2022 - this.birthYear);
};
const Annie = new Person('Annie', 1997);
Annie.calcAge(); //25

看程式碼,說故事囉!

  1. Person.prototype 裡面可以放入所有想要讓Person的實例共同享有的屬性和方法,例如範例中所有透過Person創建出來的物件都能夠使用calcAge()這個方法。
  2. 透過.hasOwnProperty()可檢查某方法屬性是否存在於該物件當中:Annie.hasOwnProperty('calcAge'); //false。那就奇怪了,既然calcAge並不在Annie物件裡面,它怎麼能夠使用呢?
  3. 還記得前面談到使用new建立物件時的第三個步驟嗎~

空物件的 .__proto__ 屬性,和 constructor function 的 prototype 屬性產生連結

也就是說 Annie.__proto__ 會連結到 Person.prototype,而我們剛剛已經把 calcAge 丟進了這個 Person.prototype 共享區域。
因此,就算在Annie物件裡面找不到 calcAge 沒關係,我們可以去 Person.prototype 找找看,找到了就不客氣地使用吧,而這個動作就是所謂的「原型繼承(prototypal inheritance / delegation)。
📌 .__proto__ 指向該物件的原型

好啊,那常聽到的 prototype chain(原型鏈)又是什麼?
簡單來說,就是我在這個物件找不到的話,我就往上面一層(也就是物件的原型)找,還是找不到的話,我就繼續往上面一層找...直到天荒地老(亂講的,直到遇見 null )。而這個透過.proto不斷往上尋找,像是有一條無形的線把物件與 prototype物件聯繫起來,就是「prototype chain(原型鏈)」。
Annie.__proto__ Ans: Person.prototype
Annie.__proto__.__proto__ Ans: Object.prototype
Annie.__proto__.__proto__.__proto__ Ans: null

2. ES6 Class

ES6新增了一個新功能:class,它的功能其實和 constructor function 類似。

JS裡面的 class 不是真正的 class ?!
JS裡面的 class 其實只是一種特殊的函式。

class PersonCl {
    constructor(firstName, birthYear){
        this.firstName = firstName;
        this.birthYear = birthYear;
    }

    //methods :自動放到.prototype裡面
    calcAge(){
        console.log(2022-this.birthYear);
    }
}
const jim = new PersonCl('Jim', 1997);
console.log(jim);
jim.calcAge(); //25

大家都說 class 是語法糖,因為它其實達到的效果跟 constructor function 沒什麼兩樣,只是它又另外幫我們做了一些事情,所以提升了便利性、程式碼看起來也更乾淨(相關的東西全都包在一起了)。

譬如,若要在 constructor function 加入 methods 時,我們得另外在外面利用 .prototype 加入,但class則可以直接把method包在裡面,因為它在你看不到的地方偷偷地把methods丟到 .pototype 裡面了!

不信嗎?你可以用它檢查一下:console.log(jim.__proto__ === PersonCl.prototype)

Q: 我用 class 了,但我還是想要在外面透過 .prototype 加入 methods 可以嗎?
A: ...你想要的話也是可以啦...。

PersonCl.prototype.greet = function(){
    console.log(`Hey ${this.firstName}`);
}
jim.greet(); //Hey Jim

其他你應該要知道的 class 特點

  1. class 不會 hoisiting
    意思是你要先把 class 創建出來,才能使用!
  2. class 是頭等公民(first-class citizen)
    意思是它可以被當作參數傳入函式也可以作為函式回傳值
  3. class 在嚴格模式(strict mode)下執行
    意思是不管你有沒有寫上 use strict,class 就是會自己在你看不到的地方變成嚴格模式來執行

class 裡面的 setter & getter
之後有空好好地研究一番

class 裡面的 static method
class中,任何前面加上static的method只會存在該class中,意思是其他instance都無法使用這個method。

3. Object.create()

const PersonProto = {
    calcAge(){
        console.log(2022-this.birthYear);
    },

    init(firstName, birthYear){
        this.firstName = firstName;
        this.birthYear = birthYear;
    },
};
const selena = Object.create(PersonProto);
selena.init("Sarah", 1992);
selena.calcAge(); //30

看程式碼,說故事囉!

  1. 首先,先定義一個物件 PersonProto 作為原型
  2. 接著利用 Object.create() 建立另一個新實體物件 selena,並將 PersonProto 作為參數傳入
  3. 噹啷!我們就完成了一個以PersonProto為原型的實體物件selena了

檢查看看 PersonProto 是否真的為 selena 的原型:
console.log(selena.__proto__ === PersonProto); //true


以上是上課時邊整理出來的陽春筆記,還有很多不清楚的地方和可以深入研究的主題!


#class #javascript #Udemy #prototype







Related Posts

逆向、組語相關雜七雜八堆放處

逆向、組語相關雜七雜八堆放處

筆記、程式設計概論

筆記、程式設計概論

【單元測試的藝術】Chap 8: 好的單元測試的支柱

【單元測試的藝術】Chap 8: 好的單元測試的支柱


Comments