Photo by Zhouxing Lu on Unsplash

Javascript ProtoType & Inheritance

Hao
Gemini Open Cloud 雙子星雲端
10 min readOct 12, 2021

--

Javascript 是一個相當獨特的程式語言,對比其它的程式語言來看,它有許多的特性與其它的程式語言截然不同。

比方說,Javascript 採用了弱型別,變數規則的隱含轉換常令人摸不著頭緖。弱型別雖然在開發上增加了相當的便利性,但也在除錯上增加了相當的負擔。

比方說,Javascript 有種特性叫 Hoisting,你可以在宣告變數或函式之前使用變數或函式。相較其它程式語言必須先宣告再使用多添了一些彈性。

比方說,幾乎大部份的程式語言都有著 class 的概念,但是 Javascript 沒有。
一直到了 ES6 ,Javascript 才有 class 的語法出現,但是這個也不是其它程式語言中的 class,而是一種語法糖,實作上與真正的 class 大相逕庭。

既然 Javscript 沒有 class,那它是怎麼樣去達到像 class 一樣的效果呢?

答案是 Prototype。

什麼是 Prototype?

打個比方,你有一雙好看的大眼睛。你想找出這是誰遺傳給你的,於是你循著族譜一路往上追尋,然後發現原來你的祖父也有一雙相同的眼睛

在這個比喻裡,你就是 Javascript 的 object,漂亮的眼睛就是 Prototype,族譜就是 __proto__。

什麼意思?

在 Javascript 裡面,所有的 object 都有 prototype 的屬性,你可以另外建立一個 object 繼承 prototype 裡屬性及函式。

怎麼做?

很簡單,我們可以在瀏覽器中做個測試

以下測試使用 chrome 的開發者工具進行

你可以先建立一個空物件如下:

當你輸入 objA. 的時候,chrome 會列出所有可以用的屬性及函式。

等等,這邊有個奇怪的地方,objA 明明是空物件,為什麼可以使用 toString、valueOf 之類的函式呢?

其實在這裡 Javascript 偷偷幫你做了一些事情,當你建立 object 的時候,它隱含地幫你繼承了 Object.prototype,所以你才能使用 Object 所提供的函式。

怎麼證明這件事呢?

你可以在開發者工具輸入 Object.prototype 來看 Object prototype 提供了什麼函式給你。

你可以發現這些函式,恰好就是 objA 所能使用的函式。

以此類推,當你建立數字的時候,隱含繼承了 Number.prototype;當你建立 Function 的時候,隱含繼承了 Function.prototype。

正是因為 Javascript 自動繼承了這些 prototype,所以你才能方便地使用這些沒有定義在物件內的函式。

現在你已經比較暸解 prototype 了,那麼一開始提到的 __proto__ 又是什麼?

剛剛說 __proto__ 像是族譜,因為它可以幫助你找到被你繼承的物件的 prototype,就像你可以透過族譜找到你的父輩一樣。它儲存的是被你繼承的物件的 prototype。

以剛剛的測試為例,objA 繼承了 Object 的 prototype,因此 objA.__proto__ 便會指向 Object.prototype

你可以在開發者工具中看見這點。

暸解這些後,你是否會好奇 Object.prototype 的 __proto__ 又會指向哪裡?

我猜你可以自行在開發者工具裡找到答案。

簡單總結一下:

  • prototype 一個物件包含了屬性及函式,這些屬性及函式可以繼承給另一個物件,使另一個物件可以使用這些屬性及函式
  • __proto__ 會指向你所繼承的物件的 prototype

所以我說,那個繼承呢?

有了上述的概念之後,我們來談談不使用那些方便的語法糖 ( 像是 class、extends ) 的話要怎麼實作繼承。

考慮以下的程式碼,你可以在 chrome 的開發者工具中的 Source 頁面來執行程式碼。

function Employee(parameter = {}) {
this.name = parameter.name || '';
this.dept = parameter.dept || '';
}
Employee.prototype.getSalary = function(salary) {
if (salary <= 1000) console.log('Monkey get a banana');
else console.log(this.name, 'get', salary, 'dollars');
}
function Engineer(parameter = {}) {
Employee.call(this, parameter);
this.projects = parameter.projects || [];
}
Engineer.prototype = Object.create(Employee.prototype);
let hao = new Engineer({
name: 'Hao',
dept: 'Frontend',
projects: ['project1', 'project2'],
});
hao.getSalary(1000);

接下來會一步一步講解這些程式碼做了什麼事情,以及它是如何達到繼承的目的。

首先,我建立了一個 Employee 的 Function,並在裡面使用 this 去儲存 name & dept。

function Employee(parameter = {}) {
this.name = parameter.name || '';
this.dept = parameter.dept || '';
}

this 在這裡的意義就是當有人使用 new 或 Object.create 來建立一個新的 object 的時候,this 可以代表這個 object instance。

另外我在 Employee 的 prototype 上定義了 getSalary 來讓後面的 function 可以繼承。

Employee.prototype.getSalary = function(salary) {
if (salary <= 1000) console.log('Monkey get a banana');
else console.log(this.name, 'get', salary, 'dollars');
}

接下來我定義了另一個 Function 叫 Engineer,在這個 Function 之中,我使用了 call 這個 Function,這個 Function 是定義在 Function.prototype 中的一個函式。

function Engineer(parameter = {}) {
Employee.call(this, parameter);
this.projects = parameter.projects || [];
}

它的用途主要是呼叫使用 Engineer 的 this 去呼叫 Employee 這個函式,這樣一來,Engineer 就會擁有和 Employee 一樣的屬性。

再來是最關鍵的一步,我們將 Engineer 的 prototype 指定為一個 object,這個 object 是由 Employee 的 prototype 所 create 出來的 object。

Engineer.prototype = Object.create(Employee.prototype);

什麼意思?

在說明這個之前,我們得先理解 Object.create 的作用是什麼。

Object.create 接受一個物件,並建立一個新的物件。跟單純的 clone 不一樣的是,Object.create 處理了 __proto__,並且綁定了 this。

簡單來說, Object.create 所產生的 object,會把該 object 的 __proto__ 指向 Object.create 所接受的參數。

test 的 __proto__ 指向了 Employee.prototype

所以這行就表示建立一個新的 prototype,並把 Engineer prototype 的 __proto__ 指向 Employee.prototype。

這樣一來,當在 Engineer.prototype 找不到函式時,Javascript 就會順著 __proto__ 去 Employee.prototype 去找函式,進而達到了繼承的效果。

Classes

ES6 引進了新的 classes 語法,讓我們可以跟上面那種複雜的寫法說拜拜。
如果上面的例子用 classes 改寫的話,大概如同下列程式碼。

class Employee {
constructor(parameter) {
this.name = parameter.name || '';
this.dept = parameter.dept || '';
}
getSalary(salary) {
if (salary <= 1000) console.log('Monkey get a banana');
else console.log(this.name, 'get', salary, 'dollars');
}
}
class Engineer extends Employee {
constructor(parameter) {
super(parameter);
this.projects = parameter.projects || [];
}
}
let hao = new Engineer({
name: 'Hao',
dept: 'Frontend',
projects: ['project1', 'project2'],
});
hao.getSalary(1000);

這樣的寫法比起剛剛 Function prototype 的寫法來說相對容易理解,而且也已經相當近似其它程式語言的寫法了,但是其背後的實作還是由 Prototype 達成。

做些測試便會發現,class 語法所產生出來的 prototype 及 __proto__與原本的並無二致,也呼應了一開始的語法糖的說法。

結語

Javscript 的 prototype 因為概念與一般的 class-based 導向的程式語言不同,初見時相當容易使人困惑,研究一陣子就會比較容易理解。

本文試著以較為簡單的角度說明 prototype ,期望能幫助困惑於 prototype 的人。文章中若有錯誤或是說明不清的地方,還請不吝指教。

--

--

Hao
Gemini Open Cloud 雙子星雲端

重度貓奴,在軟體苦海中靠著吸貓載浮載沉。 體認到沒有什麼是吸一次貓不能解決的,如果有,那就吸兩次。 目前任職於雙子星雲端,專注於開發使用者介面、前後端相關技術及為貓貓提供更好的生活。