Jeff的隨手筆記

學習當一個前端工程師

0%

『JavaScript 基礎』 Function constructor(函式建構式)

Imgur

這是一門在Udemy 課程,是同學介紹的。主要是因為想要運用到過年前這段時間好好的增進自己的JavaScript的基礎能力,讓自己能往前端工程師更近一步。主要還是會以筆記的形式做呈現!


|前言

建立object的方式在之前有說過兩個方式:

1.使用 object literal,也就是大括號的方式來建立物件。

2.使用 new Object( )的方式。

但除了上述兩個方式之外,我們還可以使用function constructor(函式建構式)的方式來建立物件。

|Function Constructor

1
2
3
4
5
6
7
function Person (){
this.firstName = 'John';
this.lastName = 'Doe';
}

var john = new Person();
console.log(john);

我們看一下輸出:

Imgur

透過 new 它會幫我們建立一個物件,然後在Person這個function裡面有了屬性名稱跟屬性值。
課堂有提到,這只是construct objects的方式不同而已,這個方式才是比較好的方式(上一篇的方式只是為了展示它如何作業的)。

還記得之前有看過這張表嗎?
Imgur

new其實是運算子(operators)的其中一種,所以當我們使用 new 這個運算子時,會先有一個空的物件被建立。

我們知道,當函式被調用時,在execution context 中會有 this 被建立,而當我們使用 new 的時候,函式裡面的 this 會被指定成剛剛所建立的那個空物件。

所以當Person這個函式被invoke(調用)時,是在幫這個空物件賦予屬性名稱和屬性值。

我們看一下2個範例,來更了解這個執行的過程:

1
2
3
4
5
6
7
8
9
function Person() {
console.log(this);
this.firstName = "John";
this.lastName = "Doe";
console.log('This function is invoked')
}

var john = new Person();
console.log(john);

Imgur

在這個範例我們可以確認兩件事情:

  1. new 會幫我們建立一個空的物件。
  2. 這個function有確實被invoke。

我們看下一個範例:

1
2
3
4
5
6
7
8
9
10
function Person() {
console.log(this);
this.firstName = "John";
this.lastName = "Doe";
console.log('This function is invoked')
return {greeting: 'i got in the way'}
}

var john = new Person();
console.log(john);

Imgur

我只是讓這個函式return 一個物件,結果最後執行結果不再是原本的,而是會改成我們新建立的物件。

因此我們可以說,只要這個函式建構式 Person 沒有指定 return 其他的物件,它就會直接回傳給我們設置this的變數。

如果我們這時候再新增一個object jane:

1
2
3
4
5
6
7
8
9
10
11
function Person() {
console.log(this);
this.firstName = "John";
this.lastName = "Doe";
console.log('This function is invoked')
}

var john = new Person();
console.log(john);
var jane = new Person();
console.log(jane);

Imgur

我們會看到它們都指向同一個function,因此我們把code稍微做個修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 jane = new Person('Jane', 'Doe');
console.log(jane);

Imgur

只要我們讓物件的屬性值變成參數,就能透過function constructor 建立出許多同屬性名稱但不同屬性值的物件。

所以要記得:
1.函式建構式(function constructor)就是普通的 function,只是我們可以透過這個 function 來建立物件。
2.透過在 function 前面加上 new 這個運算子,它會把函式中 this 這個關鍵字建立成一個新的物件,然後如果你沒有在該函式的最後指定回傳出其它物件的話,它就會自動回傳這個新的物件給你。

補充:如果在撰寫程式碼時,忘記加上 new 的話:

1
2
3

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

Imgur

這時候的this就會指向window,因此我們可以看到firstnamelastname出現在裡面。

至於為什麼會回傳undefined則是因為如果沒有說function要return什麼的話,預設就是會return undefined。

|設定物件原型

剛剛我們提到了怎麼用 Function Constructor搭配關鍵字 new來建立物件,現在來學用 Function Constructor來設定原型。

之前有提到,在 JavaScript 中的 function其實也是一種物件,其中包含一些屬性像是該函式的名稱(Name)和該函式的內容(Code),但其實 function 這裡面還有一個屬性,這個屬性稱做 prototype,這個屬性會以空物件的型式呈現。

課堂上有一段話:

簡單來說就是函數有一个原型属性,默认为空对象,除非它用作function constructor,否则它就只是待在那里。(chatGPT AI說的)

這邊就出現一個很困惑的一段話:

函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype。

我們透過下方的code來了解一下這句話的意思

1
2
3
4
5
6
7
8
9
10
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);

Imgur

如上圖所示,我們打上Person.prototype後,出現了{constructor: ƒ}
,表示 Person函式有一個原型物件,其中包含一個名稱為 “constructor” 的函式。

而這個 “constructor” 函式就是我們所定義的 Person函式,它是一個function constructor,用於創建物件的實例。

因此,當我執行 var john = new Person("John", "Doe");var jane = new Person("Jane", "Doe");時,實際上是創建了兩個繼承自 Person.prototype的物件實例,它們共享了一個function constructor和一些公共屬性。

之後我們再加入一段code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);

Imgur

我們可以看到Person.prototype多出了一個名為getFullName的函式,而且在Person函式中也出現了一個getFullName,還記得剛剛有提到:

函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype。”

因此,透過這個方式,我們就可以完成以下的code,讓我們在最後新增1行code:

1
console.log(john.getFullName());

我們就可以得到:

Imgur

|Prototype 的實用處

至於這樣做有什麼好處呢?我們可以想想,當我今天有 1000 個物件是根據這個function constructor所建立,而今天我有一個method(方法)想要讓大家使用,我就可以用這個方式。

但為什麼我不能直接加在函式裡面就好,還要透過這個方式呢?

這就要牽扯到效能的問題了,我們當然可以把它直接放在函式裡,但不要忘了:

Functions in JavaScript are objects

他們是會佔據記憶體空間的,因此如果我有1000個物件,就表示我會因為這個原因而需要1000個空間來放這個method(方法),但如果我今天是使用是建立在 prototype 中,我們只需要一個空間來存放method(方法)。

所以,為了效能上的考量,通常會把method(方法)放在建構式的 prototype 中,因為它們可以是通用的;把property(屬性)放在建構式當中,因為每一個物件可能都會有不同的屬性內容,如此將能有效減少記憶體的問題。

|Prototypal Inheritance(原型繼承)

最後,我們來談談prototypal inheritance,還記得我們之說過繼承可以分成兩種,一種是 classical inheritance,這種方式用在 C# 或 JAVA 當中;另一種則是 JavaScript 所使用的,是屬於 prototypal inheritance。

這邊會介紹一個大部分瀏覽器都支援的語法:Object.create()

看一下下面這個例子:

1
2
3
4
5
6
7
var Person = {
firstName: 'Default',
lastName: 'Default',
getFullName: function () {
return this.firstName + " " + this.lastName;
}
}

注意這邊用的是this.firstName,他會指向Person這個object,但如果沒用this的話,他則是會在getFullName 這個 execution context 中去找我們需要的變數,找不到就會到最外層的全域環境去找。

這時候我們加入Object.create():

1
2
var john = Object.create(Person);
console.log(john);

Imgur
我們可以從上面這張圖知道,john會是一個空物件,但是它繼承了 Person 這個物件當中的屬性和方法。

接下來我們把code修改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Person = {
firstName: 'Default',
lastName: 'Default',
getFullName: function () {
return this.firstName + " " + this.lastName;
}
}

var john = Object.create(Person);

john.firstName = 'John';
john.lastName = 'Doe';
console.log(john);
console.log(john.getFullName());

輸出結果為:

Imgur

之所以會這樣,是因為prototype chain(原型鍊)。

我們先從圖片的第一行來說,因為 firstName 和 lastName 在該物件已經有這兩個屬性

1
2
john.firstName = 'John';
john.lastName = 'Doe';

因此它不會在往該物件的原型去尋找。

而對 getFullName 來說,因為在 john 這個物件裡沒有這個方法,於是就會到 prototype 裡面去找,最後會回傳 “John Doe”。

透過 Object.create() 這種方法,是最單純使用 prototypal inheritance 的方式。如果你想要一個物件的原型,就先建立一個 A 物件當做其他物件的基礎,然後再建立另一個空物件 B,指稱 A 物件當做它的原型,在透過為 B 物件賦予屬性或方法。