2016年6月22日

[筆記] 談談 JavaScript 中的 function constructor 和 prototype 的建立

img
在上一篇筆記中我們說明了如何透過函式建構式(function constructor)搭配關鍵字 new 來建立物件,但其實這樣只學了一半,在這篇我們會補齊另一半,說明 function constructor 如何用來設定該物件的原型(prototype)
我們之前有提到,在 JavaScript 中的函式其實也是一種物件,其中包含一些屬性像是該函式的名稱(Name)和該函式的內容(Code),但其實 function 這裡面還有一個屬性,這個屬性稱做 prototype,這個屬性會以空物件的型式呈現。
除非你是把 function 當做 function constructor 來使用,否則這個屬性就沒有特別的用途;但如果你是把它當做 function constructor,透過 new 這個關鍵字來執行這個 function 的話,它就有特別的意義了。
要進入這個 function 的 prototype 屬性只要直接透過 .prototype 就可以了。
然而,有一點很容易令人困惑的地方,我們會以為如果我使用 .prototype 時,就可以進入這個函式的原型,但實際上並不是這樣的!
函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype,聽起來好像有聽沒有懂...沒關係,讓我們來看一些程式碼幫我們理解這個概念。

說明函式中的 prototype 屬性

1. function 中的 prototype 屬性一開始是空物件

我們先執行上篇筆記最後所寫的程式碼:
function Person(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

var john = new Person('John', 'Doe');
console.log(john);

var jane = new Person('Jane', 'Doe');
console.log(jane);
到 Google Chrome 的 console 視窗中,我們輸入 Person.prototype 得到的結果會得到一個空物件,如下圖:

2. 透過 function constructor 所建立的物件會繼承該 function 中 prototype 的內容

接著,讓我們在 Person.prototype 裡面增加一個 getFullName 的函式:
function Person(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function() {
  return this.firstName + ' ' + this.lastName;
}

var john = new Person('John', 'Doe');
console.log(john);

var jane = new Person('Jane', 'Doe');
console.log(jane);
在上面的程式第 7 - 9 行中,我們為 Person.prototype 添加了一個函式,所以當我們在 Google Chrome 的 console 視窗中呼叫 Person.prototype 時,會多了這個函式在內:
剛剛,我們有提到很重要的一句話,「函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype」。
翻成程式可能比較好說明,這句話的意思是說 Person.prototype 並不是 Person.__proto__,但是所有透過 Person 這個 function constructor 所建立的物件,在該物件實例的 __proto__ 中,會包含有Person.prototype 的內容。
也就是說,當我們使用 new 這個運算子來執行函式建構式時,它會先建立一個空物件,同時將該建構式中prototype 這個屬性的內容(Person.prototype),設置到該物件實例的 prototype 中(john.__proto__)。
因此,當我們在 Google Chrome 的 console 中輸入 john.__proto__ 時,我們就可以看到剛剛在Person.prototype 所建立的函式 getFullName 已經繼承在裡面了:

實際運用

由於 Person.prototype 中的方法已經被繼承到由 Person 這個 function constructor 所建立的物件實例 john 中,所以這時侯,我們就可以順利的使用 john.getFullName 這個方法:
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function(){
  return this.firstName + ' ' + this.lastName;
}

var john = new Person('John', 'Doe');
console.log(john);
console.log(john.getFullName());
如此,可以正確的執行 getFullName 這個函式並得到如下的結果:

透過函式建構式與 Prototype 的實用處

透過這樣的方法,我們可以讓所有根據這個函式建構式(function constructor)所建立的物件都包含有某些我們想要使用的方法。如果我們有 1000 個物件是根據這個函式建構式所建立,那麼我們只需要使用 .prototype 這樣的方法,就可以讓這 1000 個物件都可以使用到我們想要執行的某個 method。
有的人可能會好奇說,為什麼我們不要把 getFullName 這個方法直接寫在函式建構式當中呢?
❗️ 我們不該把方法放在 function constructor 中。
把方法放在函式建構式中這麼做雖然程式仍然可以正確執行並得到結果,但是這麼做會有個問題,如果我們是把這個方法直接寫在函式建構式中,那麼每一個物件都會包含有這個方法,如果我們有 1000 個物件根據這個函式建構式所建立,那麼這 1000 個物件都會包含這個方法在內,如此將會占據相當多的記憶體;但如果是建立在 prototype 中,我們只會有一個這樣的方法。
所以,為了效能上的考量,通常會把方法(method)放在建構式的 prototype 中,因為它們可以是通用的;把屬性(property)放在建構式當中,因為每一個物件可能都會有不同的屬性內容,如此將能有效減少記憶體的問題。

程式範例

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function () {
  return this.firstName + ' ' + this.lastName;
}

var john = new Person('John', 'Doe');
console.log(john);
console.log(john.getFullName());

Person.prototype.getFormalFullName = function () {
  return this.lastName + ',' + this.firstName;
}

var jane = new Person('Jane', 'Doe');
console.log(jane);
console.log(jane.getFormalFullName());

資料來源

0 意見:

張貼留言