iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 27
0
Modern Web

教練我想學 JavaScript 系列 第 27

Day 27 函數建構子與 prototype

透過函數建構子能夠建立物件以及設定物件的的屬性與方法,那要如何設定物件的原型屬性呢?

回顧一下昨天透過函數建構子建立物件的程式碼,
程式碼如下:

function Person(firstname, lastname) {
  console.log(this);
  this.firstname = firstname;
  this.lastname = lastname;
  console.log('This function is invoked!');
}

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

var jimmy = new Person('Jimmy', 'Huang');
console.log(jimmy);

在 Console 中的結果如下:

輸出兩個透過函數建構子建立的物件,

我在 Console 中透過點(成員取用)運算子來取用指派給變數 john 的物件的__proto__屬性,
如下圖:

會看到輸出一個函數建構子,在瀏覽器中透過物件的__proto__屬性,可以取用物件的原型屬性,
物件的原型屬性其實是來自函數的其中一個之前沒說過的 prototype 屬性 ,
之前說過在 JavaScript 中函數是一種特殊的物件,
函數除了有我們之前說的有一些內建的屬性與兩個隱藏屬性之外,
其實函數還有一個屬性叫作 prototype
如下圖:

圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 6 節講座 58 影片截圖

這個函數的 prototype 屬性只有在 透過 new 運算子呼叫函數時,才會用到,不然 prototype 屬性就只是待在那,
透過 new 運算子建立空物件後在 new 運算子後面的函數在透過 this 設定屬性與方法時,函數裡面的prototype屬性會添加給這個透過 new 運算子建立的空物件,
因為這個函數的 prototype屬性專門用來給透過 new 運算子呼叫函數建立的物件,
函數的prototype屬性不是函數自己的原型屬性,而是透過函數建構子建立的物件的原型屬性,

實際透過函數建構子的prototype屬性來添加物件的方法,
程式碼如下:

function Person(firstname, lastname) {
  console.log(this);
  this.firstname = firstname;
  this.lastname = lastname;
  console.log('This function is invoked!');
}

Person.prototype.getFullName = function() {
  return this.firstname + ' ' + this.lastname;
}

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

var jimmy = new Person('Jimmy', 'Huang');
console.log(jimmy);

在 Console 中的結果如下圖:

現在兩個物件都有我透過函數建構子的 prototype屬性添加的 getFullName 方法,

為了要證明函數裡的prototype屬性不是函數的 prototype 屬性,
是透過函數建構子建立的物件的prototype屬性,
我們在 Console 來看函數 Person 的__proto__屬性來確認這件事,
在 Console 中的結果如下圖:

接著來看指派給變數 john 的透過函數建構子建立的物件的__proto__屬性,
但要注意的是,這種使用__proto__屬性的方法要避免在程式碼中撰寫,
這邊只是方便展示原型屬性用,
在 Console 中的結果如下圖:

透過點(成員取用)運算子來取用指派給變數 john 的透過函數建構子建立的物件的原型屬性,
在指派給變數 john 的物件裡的原型屬性中看到剛才我們新增給 Person 函數的ptototype屬性裡增加的 getFullName 方法,

也可以在新增其他方法到函數建構子中,
在函數建構子中新增一個 getFormalFullName 方法,
程式碼如下:

function Person(firstname, lastname) {
  console.log(this);
  this.firstname = firstname;
  this.lastname = lastname;
  console.log('This function is invoked!');
}

Person.prototype.getFullName = function() {
  return this.firstname + ' ' + this.lastname;
}

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

var jimmy = new Person('Jimmy', 'Huang');
console.log(jimmy);

Person.prototype.getFormalFullName = function() {
  return this.lastname + ' ' + this.firstname;
}
console.log(jimmy.getFormalFullName());

在 Console 中的結果如下圖:

我透過指派給變數 jimmy 的物件想要取用 getFormalFullName 方法時在指派給變數 jimmy 的物件中的方法找不到,
所以會進到原型鏈裡面後找到原型屬性中有這個 getFormalFullName 方法可以取用,
所以最後可以得到上圖的輸出結果,

如果我們透過函數建構子建立 1000 個物件,
函數建構子能夠讓 1000 個物件都取用到透過函數建構子的 prototype屬性添加的方法,
需要注意的是我也可以直接在函數建構子中新增 getFullName 方法,
程式碼如下:

function Person(firstname, lastname) {
  console.log(this);
  this.firstname = firstname;
  this.lastname = lastname;
  this.getFullName = function() {
    return this.firstname + ' ' + this.lastname;
  }
}

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

var jimmy = new Person('Jimmy', 'Huang');
console.log(jimmy);

Person.prototype.getFormalFullName = function() {
  return this.lastname + ' ' + this.firstname;
}
console.log(jimmy.getFormalFullName());

在 Console 中的結果如下圖:

直接在函數建構子中透過 this 也可以添加透過 new 運算子建立的空物件的方法,
現在物件也可正常取用 getFullName 方法輸出正確的結果,

但這樣有個問題,透過在函數建構子裡添加方法,會讓每個建立出來的物件裡本身都有這個 getFullName 方法,
這會浪費記憶體空間,

在函數建構子中添加屬性是沒問題的,因為每個物件的屬性值都不一樣,

解決的方法是只在函數建構子的prototype屬性裡添加方法,
這樣就算你透過函數建構子建立再多個物件也只會有一個 getFullName 方法,
每個物件在本身的方法裡找不到,就會進到原型鏈找透過函數建構子建立的物件的原型屬性裡找到,
在函數建構子的prototype屬性裡添加方法可以幫助我們節省記憶體空間,

現在我們知道如何在函數建構子裡添加prototype屬性,
以便讓函數建構子建立出來的物件可以進到原型鏈找到函數建構子設定給用 new 運算子建立出來的空物件的prototype屬性進而找到原型屬性裡面的方法了,

內建的函數建構子

JavaScript 有一些內建的函數建構子(Number 、String、Date等),
這些函數建構子建立物件時都有設定prototype屬性給在建立物件時,prototype屬性內都有添加方法給要建立的物件,
現在到 Console 中來看,透過 new 運算子來呼叫 Number 這個內建的函數建構子來建立物件,
如下圖:

你可能會以為結果會輸出數值 3(純值),但不是,
記得在呼叫函數前加上 new 運算子會回傳物件,這個物件裡有一個 PrimiteveValue 的屬性,這個屬性才是純值,
所以現在回傳一個物件給變數 a,

如果我用點(成員取用)運算子來取用 Number 的prototype 屬性,
會看到用來處理 Number 的一些方法,
如下圖:

JavaScript 在 Number 函數建構子的prototype屬性增加了許多屬性與方法,
當用 new 運算子呼叫函數建構子建立物件時,會將這些方法設定(新增)給物件的原型屬性,
在使用這些方法時在本身物件中找不到所以會進到原型鏈找原型屬性內的方法,
所以在物件被建立完指派給變數 a 後,
變數 a 的原型屬性裡也會有在 Number 函數建構子的prototype屬性裡的屬性與方法可以使用,
如下圖:

接著來看另一個內建的函數建構子 String,
我們一樣用 new 運算子來呼叫 String 函數建構子並回傳物件,
如下圖:

變數 a 的值不是字串(純值)而是一個物件,
在物件中有一個 PrimitiveValue 屬性,這個屬性才是純值,

變數 a 有一些可以在字串上使用的方法,
如下圖:

這些屬性與方法並不是變數 a 本身的,變數 a 現在是一個透過 new 運算子與函數建構子回傳的物件,
物件本身找不到這些屬性與方法因此進入原型鏈找原型屬性,在原型屬性中找到的,
物件在透過函數建構子建立時,被函數建構子的prototype屬性添加了這些屬性與方法給物件的原型屬性,

在 String 函數建構子的prototype屬性裡使用 indexOf 方法查找某個字母是否存在 ,
在 Console 的結果如下圖:

indexOf 方法回傳 -1 代表這個字母不在String 函數建構子的prototype屬性中,
如果直接透過指派給變數 a 的物件的原型屬性裡的 indexOf 方法就可以找到,
如下圖:

在某些例子中,JavaScript 知道你要的是物件不是純值,
所以字串可以直接使用 String 物件的一些屬性與方法,
如下圖:

字串本身沒有這些屬性與方法,JavaScript 會自動幫我們把字串傳入給透過 new 運算子呼叫的 String 函數建構子回傳的物件,現在字串被包在這個回傳的物件裡,
因此我們才可以使用一些物件中的屬性與方法,
如下圖:

JavaScript 在看到字串想要使用 length 時,其實自動替我們做了上圖的事情,

在來看一下 Date 函數建構子,
一樣透過 new 運算子呼叫它,這會傳回一個物件,
如下圖:

變數 a 的值現在是一個 Date 物件,
有一些 Date 的屬性與方法可以使用,
如下圖:

變數 a 的值是透過 new 運算子呼叫 Date 函數建構子回傳的物件,
這個物件本身並沒有這些屬性與方法,是 Date 函數建構子的prototype屬性在回傳物件時設定給這個物件的原型屬性,如下圖:

物件本身找不到屬性與方法就會往原型鏈找連結的原型屬性裡的屬性與方法,

所以透過 new 呼叫函數運算子會回傳物件,
因為這個特性,我們可以添加一些功能給 Number 、String、Date、物件、陣列與函數,

我在 String 函數建構子的prototype屬性中透過點(成員取用)運算子添加一個比較字串長度是否大於某數的方法
需注意方法的名稱不要與prototype屬性裡原有的方法名稱重複,這會覆蓋掉原本的方法,
程式碼如下:

String.prototype.isLengthGreaterThan = function(limit) {
  return this.length > limit;
}

console.log('John'.isLengthGreaterThan(3));

在 Console 中的結果如下圖:

記得函數建構子的名稱首字都是大寫嗎?
這些內建的函數建構子除了本身有些屬性與方法,
也可以在而外手動透過函數建構子的 prototype屬性添加屬性或方法,

接著在 Number 函數建構子中添加屬性或方法,
程式碼如下:

Number.prototype.isPositive = function() {
  return this > 0;
}

console.log(1.isPositive());

在 Console 中的結果如下圖:

這是因為 JavaScript 不會自動將數值轉換成物件,

我們需要透過 new 運算子呼叫函數建構子來回傳一個物件並指派給變數,
程式碼如下:

Number.prototype.isPositive = function() {
  return this > 0;
}

var a = new Number(1);

console.log(a.isPositive());

在 Console 中的結果如下圖:

透過 new 運算子除了建立的空物件,
也會在呼叫函數產生的執行環境被創造時讓產生的 this 指向 new 運算子建立的空物件,
因為內建的 Number 函數建構子的prototype屬性有先設定了許多數值可以使用的屬性與方法,
以及我在函數建構子的prototype屬性手動添加的 isPositive 方法,
讓透過 new 運算子呼叫的 Number 函數建構子執行時,也將 Number 函數建構子的prototype屬性裡有的屬性與方法設定給現在 this 指向的空物件的原型屬性,
因此最後回傳的物件的原型屬性裡也有 Number 函數建構子的prototype屬性裡面的屬性與方法,
當然也包含剛才手動設定給 Number 函數建構子的prototype屬性的 isPositive 方法,

透過內建的函數建構子來建立純值有些地方要特別注意,
如果你在內建的函數建構子前面加上 new 運算子會讓純值被包進物件中,
程式碼如下:

var b = new Number(3);
console.log(typeof(b));

在 Console 中的結果如下圖:

透過 new 運算子呼叫函數建構子會回傳物件,

如果你真的要透過內建的函數建構子來建立純值,不要在內建函數建構子前面加上 new 運算子,
程式碼如下:

var c = Number(3);
console.log(typeof(c));

在 Console 中的結果如下圖:

這樣函數建構子回傳的值就會是純值,

因此需要特別注意 new 運算子對函數建構子的影響!

課堂講師也提到最好不要使用內建 Date函數運算子,最好改使用 moment.js
moment.js 是一套非常方便讓我們數理日期上計算的函式庫,

很多框架與函式庫都是透過函數建構子的prototype屬性來添加功能的,JavaScript 的原型繼承給了我們相當大的彈性與擴充性,


上一篇
Day 26 new 運算子與構造函數
下一篇
Day 28 陣列與 for in
系列文
教練我想學 JavaScript 30

尚未有邦友留言

立即登入留言