原型鏈(Prototype Chain)


原型鏈的概念較為複雜,我們可以從認識如何以「建構子函式(constructor function)」搭配「new」關鍵字建立實例(instance)開始,逐步了解如何以「物件原型(prototype)」、「原型繼承(prototype inheritance)」概念,提升記憶體使用效能,最後再看語法糖「class」如何在保留相同功能的同時,增進相關程式碼的可閱讀性。


當我們要製作許多相似物件時,可以像下方程式碼這樣土法煉鋼,把物件一個一個做出來:

let Japan = {                                      //建立「Japan」實例
    name: "Japan",
    capital: "Tokyo",
    population: 378000,
    inAsia: true,
    welcome(){
        console.log("Welcome to Japan!");
    }
}

let Finland = {                                    //建立「Finland」實例
    name: "Finland",
    capital: "Helsinki",
    population: 338000,
    inAsia: false,
    welcome(){
        console.log("Welcome to Finland!");
    }
}

console.log(Japan);                                //顯示「Japan」實例屬性
console.log(Finland);                              //顯示「Finland」實例屬性

Japan.welcome();                                   //顯示「Japan」實例方法
Finland.welcome();                                 //顯示「Finland」實例方法

顯示結果如下:

但如果要製作非常大量相似的物件,這樣一個一個把物件打出來實在太慢,有沒有更有效率的什麼方式呢?


建構子(constructor)

「建構子(constructor)」函式可讓我們避免大量打重複的程式碼,只要打好建構子函式一次,後面要製作相似物件時,都可以直接套用函式內容。
使用建構子函式時,有下列兩點要特別留意:

  1. 在為函式命名時,習慣上會讓第一個字母為大寫。
  2. 以「函式表達式」建立新物件時,使用「new」這個關鍵字,新建立的物件稱為「實例(instance)」。

舉例來說,如果要如上述程式碼製作多個不同國家的物件,就可以先做一個名為「Country」的建構子函式,例如:

function Country(name, capital, area, inAsia){
    this.name = name,
    this.capital = capital,
    this.area = area,
    this.inAsia = inAsia,
    this.welcome = function(){
        console.log("Welcome to " + this.name + "!");
    }
}

要製作新的實例時,就先為新實例賦予一個變數名稱、完成函式表達式,並在「Country」前加上「new」這個關鍵字,後面再依序輸入該實例的參數如下:

let Japan = new Country(“Japan”, “Tokyo”, 378000, true);

這裡輸入的「"Japan”」就是建構子函式的「name」這個參數,於函式內賦值於「this.name」,這樣「Japan」這個實例的「name」屬性就會是「"Japan”」,其他三個參數亦同。

"Japan"就是以Country建構子函式建立Japan實例的name屬性
至於若要顯示這個實例當中的方法,則一樣使用點記法(dot notation)如下:

Japan.welcome();

上述概念的完整程式碼如下,這裡再多建立一個名為「Finland」的實例:

function Country(name, capital, area, inAsia){                             //建立名為「Country」的建構子函式
    this.name = name;
    this.capital = capital,
    this.area = area,
    this.inAsia = inAsia,
    this.welcome = function(){
        console.log("Welcome to " + this.name);
    }
}

let Japan = new Country("Japan", "Tokyo", 378000, true);                   //套用建構子函式,製作名為「Japan」的實例

let Finland = new Country("Finland", "Helsinki", 338000, false);           //套用建構子函式,製作名為「Finland」的實例

console.log(Japan);                                                        //顯示「Japan」實例屬性
console.log(Finland);                                                      //顯示「Finland」實例屬性

Japan.welcome();                                                           //顯示「Japan」實例方法
Finland.welcome();                                                         //顯示「Finland」實例屬性

有了「Country」這個建構子函式,只要建立函式表達式、並輸入對應的參數,就可以做出大量實例,不必重複打出物件的完整程式碼。上述程式碼顯示結果如下:

我們可以再分析顯示結果是如何產生的:


物件原型(prototype)

雖然可以透過建構子函式快速建立多個實例,但這些實例全部都是分開存在記憶體中,使記憶體多個位置都儲存相同資料,十分浪費儲存空間。

以上述的例子來說,兩個實例都有同樣的「welcome()」方法,卻是儲存在記憶體中不同的地方:

既然兩者是一樣的東西,我們希望可以把這個方法放在記憶體的其中一個地方就好,不同實例要使用時,都可以去同一個位置讀取並執行該方法,避免浪費記憶體空間。

物件原型(prototype)

在 JavaScript 中,每個物件都有一個稱為「物件原型(prototype)」的屬性,代表該物件適用於所有情況的屬性與方法。

舉例來說,當我們以「new」搭配「Country()」建立「 Japan」實例時,除了有「Japan」本身的「area: 378000」、「capital: “Tokyo”」、「inAsia: true」、「name: “Japan”」四種屬性之外,也會有「Country.prototype」的所有屬性與方法;而由於「Country.prototype」是一種物件,因此也適用「Object.prototype」的所有屬性與方法,三者間有繼承(inherit)關係。

仔細觀察程式碼執行結果,也能看到這樣的繼承關係:

透過綁定建構子與「.prototype」,並套上新的方法,就能讓該方法適用於所有建立的實例上。以本文的範例而言,在「Country.prototype」中新增的方法即可套用於所有以「new」搭配「Country()」建立的實例。

舉例來說,要讓「new Japan」與「new Finland」都共享新增的「welcome()」方法,可以先在「Country.prototype」加上「welcome」如下:

Country.prototype.welcome = function{
    console.log("Welcome to " + this.name + "!");
}

當「welcome()」方法被加進了「Country.prototype」中,不管是「new Japan」、「new Finland」或任何其他以「new」搭配「Country()」建立的實例,都可以直接取用同一個「welcome()」方法。

若檢視「new Japan」與「new Finland」兩實例的屬性與方法內容,可見「welcome()」方法已經一同被放進「Country.prototype」中。

執行後的顯示結果依然相同:


原型繼承(prototype inheritance)

如果該物件沒有某屬性或方法,可以往上一層的「prototype」找,最高找到「Object.prototype」,再上去就是 null;但上層的物件無法繼承下層物件的屬性或方法。

舉例來說,現在有個建構子函式「Person」,參數包含「name」與「gender」,用其建立的實例另設定「greeting()」方法如下:

function Person(name, gender) {
    this.name = name,
    this.gender = gender
}

Person.prototype.greeting = function(){
    console.log(this.name + " says Hi!");
}

let Andy = new Person("Andy", "male");
console.log(Andy);
Andy.greeting();

顯示結果如下:

現在我們希望以「Person」為母集合,另建立「Staff」這個子集合,參數包含「ID」與「department」如下:

function Person(name, gender) {
    this.name = name,
    this.gender = gender
}

Person.prototype.greeting = function(){
    console.log(this.name + “ says Hi!”);
}

//建立Staff建構子函式
function Staff(name, gender, ID, department){
    Person.call(this, name, gender);
    this.ID = ID,
    this.department = department
}

let Andy = new Person(“Andy”, “male”);

//以Staff建構子函式建立新實例Emily
let Emily = new Staff(“Emily”, “female”, 13, “R&D”);

console.log(Emily);
Emily.greeting();

顯示結果如下,可發現「Emily」這個實例無法讀取位在「Person.prototype」中的方法:

Object.create()-讓實例方法也能被繼承

要讓「Staff.prototype」繼承原屬「Person.prototype」的方法,需用到「Object.create()」,程式碼如下:

function Person(name, gender) {
    this.name = name,
    this.gender = gender
}

Person.prototype.greeting = function(){
    console.log(this.name + " says Hi!");
}

function Staff(name, gender, ID, department){
    Person.call(this, name, gender);
    this.ID = ID,
    this.department = department
}

//以Person.prototype為原型,建立Staff.prototype
Staff.prototype = Object.create(Person.prototype);

let Emily = new Staff("Emily", "female", 13, "R&D");

console.log(Emily);
Emily.greeting();

加上這一行後,「Emily」實例就可以一併使用「Person.prototype」的方法,顯示結果如下:

「Staff.prototype」也可以製作屬於自己專屬的方法,但該方法無法套用於以「Person」建構子函式建立的實例:

function Person(name, gender) {
    this.name = name,
    this.gender = gender
}

Person.prototype.greeting = function(){
    console.log(this.name + “ says Hi!”);
}

function Staff(name, gender, ID, department){
    Person.call(this, name, gender);
    this.ID = ID,
    this.department = department
}

Staff.prototype = Object.create(Person.prototype);

//建立專屬於Staff的方法
Staff.prototype.resign = function(){
    console.log(this.name + “ resigns.”);
}

let Andy = new Person(“Andy”, “male”);
let Emily = new Staff(“Emily”, “female”, 13, “R&D”);

//Emily實例適用greeting()與resign()兩方法
Emily.greeting();
Emily.resign();

//Andy實例僅適用greeting()方法
Andy.greeting();
Andy.resign();

上述程式碼顯示結果如下:


類別(class)

跟物件導向程式語言不同的是,JavaScript 的「class」只是語法糖,用來簡化「物件原型繼承」的程式碼,並不會形成「物件導向繼承模型(object-oriented inheritance model)」。

以上個段落「Person」建構子函式建立的實例而言,可以連同「Person.prototype」一起寫進「class」如下:

class Person{
    constructor(name, gender){
    this.name = name,
    this.gender = gender
    }

    greeting() {
        console.log(this.name + “ says Hi!”);
    }
}

let Andy = new Person(“Andy”, “Male”);
console.log(Andy);
Andy.greeting();

顯示結果與未使用「class」時相同:

extends & super

如果要用「class」加上「Staff」這個子集合,則用「extends」讓以「Staff」建立的實例繼承以「Person」建立的實例之屬性與方法,建構子函式內部用「super」繼承原「Person」屬性,表示「Person」是「Staff」的母集:

class Person{
    constructor(name, gender){
    this.name = name,
    this.gender = gender
    }

    greeting() {
        console.log(this.name + “ says Hi!”);
    }
}

//用extends使以Staff建立的實例繼承以Person建立的實例之屬性與方法
class Staff extends Person{
    constructor(name, gender, ID, department){

    //super指Person是Staff的母集(superset)
    super(name, gender);
    this.ID = ID,
    this.department = department
    }

   resign(){
       console.log(this.name + “ resigns”);
   }
}

let Emily = new Staff(“Emily”, “female”, 13, “R&D”);
Emily.greeting();
Emily.resign();

顯示結果與未使用「class」時相同:

相較於原本的寫法,使用「class」語法糖寫的程式碼比較直觀、也相對好懂許多。


static

在「class」語法糖中,可以用關鍵字「static」賦予專屬於該「class」內的屬性與方法,而非屬於新做出來的實例,例如在「Circle」這個類別中建立「Circle.Pi」屬性與「getAreaFormula()」,可以這樣寫:

class Circle{
    //static屬性:Pi
    static Pi = 3.14;

    constructor(radius){
        this.radius = radius
    }

    getArea(){
        console.log(“The area is “+ this.radius**2*Circle.Pi);
    }

    //static方法:公式
    static areaFormula(){
        console.log(“Static formula is r * r * Pi”);
    }
}

let C1 = new Circle(5);
C1.getArea();
Circle.areaFormula();

顯示結果如下:


參考資料

  1. 該來理解 JavaScript 的原型鍊了
#javascript #原型鏈







你可能感興趣的文章

雜草叢生的部落格

雜草叢生的部落格

React(12) - useCallback & useMemo & memo

React(12) - useCallback & useMemo & memo

Remote Tool 指令

Remote Tool 指令






留言討論