在初步認識完物件的原型(Prototype)觀念後,
接著來說另一個建立物件的方法,
之前說過透過物件實體語法(兩個大括號)、或使用 new Object() 都可以快速的建立物件,
但其實在 JavaScritp 中還有第3種建立物件的方法,
就是透過函數建構子,這種方法要從 JavaScript 發明前的大環境說起,
1994年 JavaScript 的作者 Brandon Eich 因為網景公司(當時瀏覽器的霸主)的要求,
需要開發一款能讓瀏覽器與使用者互動的程式語言,
在處在一個類似跟現在一樣 Google 與 Apple 互相競爭的時代,
只是當時是幾間軟體巨擘 Netscape、 Microsoft 、Sun 、Oracle 正在進行一場程式語言霸主之爭,
如果一個程式語言如果都沒有人在使用,這樣這個程式語言很快就被市場淘汰掉了,
像是 Microsoft 為了吸引使用 Visual Basic 程式語言的開發者,推出了 VBScript,
JavaScritp 為了吸引其他像是 C# 、Java 等當時主流程式語言的開發者也來使用 JavaScript ,
將 Java 包含在自己的名稱,這些是公司的行銷手法,
甚至還讓建立物件的語法跟 Java 等程式語言類似,希望能吸引到其他程式語言的開發者,
在 C# 與 Java 等程式語言中建立物件會透過 new 來定義 Class 的屬性與方法,
而 JavaScript 則透過了類似的語法來建立物件,
語法如下:
var john = new Person();
Person() 在 C# 或 Java 中是 Class 的名稱,
所以當有在使用 Java 的開發者看到這樣子的語法時會產生親切感,覺得跟 Java 的語法很像,
讓他們能夠被吸引來使用 JavaScript 這門原本專為瀏覽器發明的程式語言,
new 是從 Java 引入的語法,
而 new 在 JavaScritp 中是一個運算子,可在之前說的運算子優先度表格中看到,
如下圖:
圖片來源:MDN 運算子優先度
透過函數建構子以及 new 運算子來建立物件有一些小地方需要特別注意,
先來看個透過 new 運算子與函數建構子建立物件的例子,
new 先建立一個函數 Person,並在函數中使用 this 添加屬性,
程式碼如下:
function Person() {
this.firstname = "John";
this.lastname = "Doe";
}
var john = new Person();
console.log(john);
在 Console 中的結果如下:
這邊我不是呼叫 Person 函數,
而是把 new 運算子放在函數前面,
new 運算子會先建立一個空物件,
就像是把空物件指派給變數 a 一樣,
程式碼如下:
var a = {};
我們都知道呼叫函數會創造執行環境,
把 new 運算子放在要呼叫的函數前會讓函數的執行環境被創造時產生的 this 在執行時指向透過 new 運算子建立的空物件,
所以呼叫函數時原來在函數裡透過 this 新增的屬性或方法會添加到空物件中,函數最終會回傳新的 Person 物件,Person 物件會有在函數中設定的屬性與方法,
我們來確認函數確實有被呼叫,
程式碼如下:
function Person() {
this.firstname = "John";
this.lastname = "Doe";
console.log('This function is invoked!');
}
var john = new Person();
console.log(john);
在 Console 中的結果如下:
會看到函數 Person 中的 console.log 文字訊息確實有被印出來,
來看看透過 new 運算子 this 是不是真的如我們說的指向一個空物件,
程式碼如下:
function Person() {
console.log(this);
this.firstname = "John";
this.lastname = "Doe";
console.log('This function is invoked!');
}
var john = new Person();
console.log(john);
在 Console 中的結果如下:
在 Console 中 this 指向是一個空物件(記憶體位址的值與new 運算子建立的空物件相同),
如果我在函數中直接回傳另一個物件,
你覺得會發生什麼是呢?
程式碼如下:
function Person() {
console.log(this);
this.firstname = "John";
this.lastname = "Doe";
console.log('This function is invoked!');
return { greeting: 'Hola! ' + this.firstname };
}
var john = new Person();
console.log(john);
在 Console 中的結果如下:
如果直接回傳一個物件,這樣就不會在回傳透過 this 新增屬性的 Person 物件了,
這會影響回傳結果,
現在我們知道透過 new 呼叫函數來建立相同的物件,
程式碼如下:
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 jimmy = new Person();
console.log(jimmy);
在 Console 中的結果如下:
在 Console 中得到兩個相同的物件,
但現在有個問題 ,物件的屬性值都一樣,
可以透過給函數參數來解決這個問題,
參數名稱最好和屬性名稱相同,方便透過參數了解添加了哪些屬性
程式碼如下:
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 中的結果如下:
現在每個物件擁有相同的屬性,不一樣的屬性值,
我們再來複習一遍函數建構子(式)
函數建構子(式)是一個正常的函數用來建立物件,
透過 new 運算子來建立一個空物件,接著在呼叫函數時因為 new 運算子的關係,
函數的執行環境創造時產生的 this 在執行時指向透過 new 關鍵字建立的空物件,
函數在執行時會知道我們用 this 新增屬性或方法到空物件,
最後會回傳這個你有新增一些屬性或方法的空物件,
函數建構子(式)被用來添加給用 new 運算子建立的空物件的屬性或方法,
另外,
在使用函數建構子(式)時有個地方需要特別注意,
如果我們忘記在呼叫函數前面加上 new 運算子,
JavaScript 會不知道你是要建立物件,
因為我們沒有透過 return 回傳任何物件,
函數執行完閉後會回傳 undefined
程式碼如下:
function Person(firstname, lastname) {
console.log(this);
this.firstname = firstname;
this.lastname = lastname;
this.getFullName = function() {
console.log('Hello ' + firstname + ' ' + lastname);
}
console.log('This function is invoked!');
}
var john = Person('John', 'Doe');
console.log(john);
var jimmy = Person('Jimmy', 'Huang');
console.log(jimmy);
console.log(jimmy.getFullName());
在 Console 中的結果如下:
由於沒有透過 new 運算子建立空物件,在呼叫函數執行環境被創造時,函數執行環境的 this 現在是指向全域物件 Window ,不是指向空物件,
因為現在函數執行完閉回傳 undefined
,現在變數的值都是 undefined
,undefined
中沒有 getFullName 方法可以取用,所以在取用方法時會得到這個錯誤訊息,
要避免在透過函數建構子(式)建立物件時忘記寫 new 運算子,
可以將函數的名稱的首字母大寫,在樣我們在看到程式碼時,就會知道呼叫的函數首字母是要建立物件,
就會在呼叫的函數前加上 new 運算子,這是一個幫助我們避免在透過函數建構子(式)時忘記加上 new 運算子的方法,
之後會介紹如何用函數建構子(式)來添加物件的原型屬性。