昨天有提到Function constructor但並沒有深入講解,今天就來補齊這個部分。
建立object的方式在之前有說過兩個方式:
1.使用 object literal,也就是大括號的方式來建立物件。
2.使用 new Object( )的方式。
但除了上述兩個方式之外,我們還可以使用function constructor(函式建構式)的方式來建立物件。
昨天有看過這個案例吧:
function Person (){
this.firstName = 'John';
this.lastName = 'Doe';
}
var john = new Person();
console.log(john);
它的輸出:
透過 new
它會幫我們建立一個物件,然後在Person這個function裡面有了屬性名稱跟屬性值。
還記得之前有看過這張表嗎?
new
其實是運算子(operators)的其中一種,所以當我們使用 new 這個運算子時,會先有一個空的物件被建立。
我們知道,當function 被調用時,在execution context 中會有 this 被建立,而當我們使用 new 的時候,function 裡面的 this 會被指定成剛剛所建立的那個空物件。
所以當Person這個函式被invoke(調用)時,是在幫這個空物件賦予屬性名稱和屬性值。
我們看一下2個範例,來更了解這個執行的過程。
範例1:
function Person() {
console.log(this);
this.firstName = "John";
this.lastName = "Doe";
console.log('This function is invoked')
}
var john = new Person();
console.log(john);
在這個範例我們可以確認兩件事情:
我們看下一個範例。
範例2:
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);
我讓這個function 回傳一個object ,最後執行結果不再是範例1的結果,而是會改成我們新建立的物件。
因此我們可以說,只要這個函式建構式 Person 沒有指定 return 其他的物件,它就會直接回傳給我們設置this的變數。
如果我們這時候再新增一個object jane:
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);
我們會看到它們都指向同一個function,因此我們把code稍微做個修改:
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);
只要我們讓物件的屬性值變成參數,就能透過function constructor 建立出許多同屬性名稱但不同屬性值的物件。
所以要記得:
1.函式建構式(function constructor)就是普通的 function,只是我們可以透過這個 function 來建立物件。
2.透過在 function 前面加上 new 這個運算子,它會把函式中 this 這個關鍵字建立成一個新的物件,然後如果你沒有在該函式的最後指定回傳出其它物件的話,它就會自動回傳這個新的物件給你。
補充:如果在撰寫程式碼時,忘記加上 new 的話:
var john = Person('John', 'Doe');
console.log(john);
!https://i.imgur.com/ygaiR0t.png
這時候的this就會指向window
,因此我們可以看到firstname
跟lastname
出現在裡面。
至於為什麼會回傳undefined
則是因為如果沒有說function要return什麼的話,預設就是會return undefined。
剛剛我們提到了怎麼用 Function Constructor搭配關鍵字 new來建立物件,現在來學用 Function Constructor來設定原型。
之前有提到,在 JavaScript 中的 function其實也是一種物件,其中包含一些屬性像是該函式的名稱(Name)和該函式的內容(Code),但其實 function 這裡面還有一個屬性,這個屬性稱做 prototype,這個屬性會以空物件的型式呈現。
在JavaScript: Understanding the Weird Parts
的課堂上有一段話:
💡 And all functions, every function, every function in JavaScript you've ever written in your life, has a prototype property that starts off its life as an empty object, and unless you're using the function as a function constructor, it just hangs out
簡單來說就是函數有一个原型属性,默認為空的物件,除非它用作function constructor,否則它就只是待在那里。(chatGPT AI翻譯)
這邊就出現一個很困惑的一段話:
💡 The prototype property on a function is not the prototype of the function. It's the prototype of any objects created if you're using the function as a function constructor.
函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype。
我們透過下方的code來了解一下這句話的意思
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);
如上圖所示,我們打上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:
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);
我們可以看到Person.prototype
多出了一個名為getFullName的函式,而且在Person
函式中也出現了一個getFullName,還記得剛剛有提到:
“函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype。”
因此,透過這個方式,我們就可以完成以下的code,讓我們在最後新增1行code:
console.log(john.getFullName());
我們就可以得到:
至於這樣做有什麼好處呢?我們可以想想,當我今天有 1000 個物件是根據這個function constructor所建立,而今天我有一個method(方法)想要讓大家使用,我就可以用這個方式。
但為什麼我不能直接加在函式裡面就好,還要透過這個方式呢?
這就要牽扯到效能的問題了,我們當然可以把它直接放在函式裡,但不要忘了:
Functions in JavaScript are objects
他們是會佔據記憶體空間的,因此如果我有1000個物件,就表示我會因為這個原因而需要1000個空間來放這個method(方法),但如果我今天是使用是建立在 prototype 中,我們只需要一個空間來存放method(方法)。
所以,為了效能上的考量,通常會把method(方法)放在建構式的 prototype 中,因為它們可以是通用的;把property(屬性)放在建構式當中,因為每一個物件可能都會有不同的屬性內容,如此將能有效減少記憶體的問題。
最後,我們來談談prototypal inheritance,還記得我們之說過繼承可以分成兩種,一種是 classical inheritance,這種方式用在 C# 或 JAVA 當中;另一種則是 JavaScript 所使用的,是屬於 prototypal inheritance。
這邊會介紹一個大部分瀏覽器都支援的語法:Object.create()
看一下下面這個例子:
var Person = {
firstName: 'Default',
lastName: 'Default',
getFullName: function () {
return this.firstName + " " + this.lastName;
}
}
注意這邊用的是this.firstName
,他會指向Person這個object,但如果沒用this的話,他則是會在getFullName
這個 execution context 中去找我們需要的變數,找不到就會到最外層的全域環境去找。
這時候我們加入Object.create():
var john = Object.create(Person);
console.log(john);
我們可以從上面這張圖知道,john會是一個空物件,但是它繼承了 Person 這個物件當中的屬性和方法。
接下來我們把code修改一下:
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());
輸出結果為:
之所以會這樣,是因為prototype chain(原型鍊)。
我們先從圖片的第一行來說,因為 firstName 和 lastName 在該物件已經有這兩個屬性
john.firstName = 'John';
john.lastName = 'Doe';
因此它不會在往該物件的原型去尋找。
而對 getFullName 來說,因為在 john 這個物件裡沒有這個方法,於是就會到 prototype 裡面去找,最後會回傳 “John Doe”。
透過 Object.create() 這種方法,是最單純使用 prototypal inheritance 的方式。如果你想要一個物件的原型,就先建立一個 A 物件當做其他物件的基礎,然後再建立另一個空物件 B,指稱 A 物件當做它的原型,在透過為 B 物件賦予屬性或方法。
呼結束了~為了這兩篇又去重新看了一次JavaScript: Understanding the Weird Parts
,並且參考了很多前輩觀看這課程的心得而得出這兩篇文章,目前我的知識含量還無法很明白地說出這些東西,希望透過這次的整理讓我更加理解。
到此本次鐵人賽原本預定的章節已經全部複習完畢,剩下兩天會針對AJAX來做一次學習。