建立物件

函數建構子、「new」與 JavaScript 的歷史

前言

前面我們已經瞭解許多物件、原型繼承、原型鏈和物件屬性及方法,接下將更深入討論建立物件。

「new」

前面我們已經有講過幾種建立物件的方式,其中一個是物件實體語法。

var jack = { firstname: 'jack', lastname: 'Dec', }

而其實還有別的物件建立方式。

var jack = new Person();

那為什麼會有 new?,這主要與 JavaScript 的誕生有關係,一個語言若都沒有人使用就代表著死亡,所以當初 JavaScript 會叫 JavaScript 就是希望可以吸引 Java 開發者,而 Java 開發者習慣使用 new 的關鍵字來定義物件等等,所以當 Java 開發者看到這樣寫會感覺比較親切,但是使用這種方式來建立物件有些許問題,但是我們還是需要瞭解,因為許多專案及原始碼都會有它的存在。

下面將用課堂的範例來做一點好玩的事情。

function Person() { this.fistname = 'John'; this.lastname = 'Doe'; } var john = new Person(); console.log(john);

我們發現 john 變成一個物件了,但是這邊我們要記得一件事情,我們只是在利用一些方法在 JavaScript 中建立物件,那為了建立物件我們必須給它屬性和方法,然後建立原型,而這是為了讓我們看更清楚而做的,所以接下來將使用比較正確的方式來增加屬性和方法、設定原型。

那 new 其實是一個運算子,為什麼這樣講呢?讓我們翻一下先前的文件>MDN 的文件 (在下方一點地方)

上面的例子中 new 會改變 this 指向的東西,this 指向到一個空的物件,而 new 建立了一個新物件,然後呼叫了 Person(),所以現在 this 變數指向 new 創造的新的空物件的記憶體位置。

所以當我們打 .firstname 和 .lastname 就會增加到空物件上,就像這樣。

var john = {}; john.fistname = 'John'; john.lastname = 'Doe'; console.log(john);

而在 JavaScript 中會回傳 new 運算子建立的物件,這會建立一個新物件,而成為物件的一部分也會回傳東西,所以我們試著這樣嘗試看看。

function Person() { this.fistname = 'John'; this.lastname = 'Doe'; console.log('我回傳囉'); } var john = new Person(); console.log(john);

由此可知,new 運算子會對這個函數呼叫,然後在回傳裡面的東西變成物件。

那如果 this 我們什麼都不對他做的話,他將會回傳本身。

function Person() { console.log(this); this.fistname = 'John'; this.lastname = 'Doe'; console.log('我回傳囉'); } var john = new Person(); console.log(john);

但是如果我們本身若對函數回傳的話,就會變成只回傳那個資料。

function Person() { console.log(this); this.fistname = 'John'; this.lastname = 'Doe'; console.log('我回傳囉'); return { greet: '我是回傳的物件' }; } var john = new Person(); console.log(john);

如果沒有回傳時,JavaScript 就會改找我們設定 this 的變數。

所以我們可以從這邊知道,我們可以透過函數來建構物件,而這叫做函數建構子。

那我們可以依照 函數建構子 來做一點進階的作法,假設今天 firstname 及 lastname 是改由我們自己傳入呢?

function Person(first, last) { this.firstname = first; this.lastname = last; } var john = new Person('John', 'Doe'); console.log(john);

所以我們可以利用函數參數的特性來傳入資料進而改變 firstname 及 lastname。

簡單來講 new 運算子可以做出新物件,函數建構子是被用來增加新物件的屬性和方法

函數建構子與「.prototype」

前言

我們已經知道函數建構子可以幫助新物件設定屬性及方法,但原型呢?所以接下來就要來做講檢該如何設定原型。

prototype

這是上一章節的範例

function Person(first, last) { this.firstname = first; this.lastname = last; } var john = new Person('John', 'Doe'); console.log(john);

當我們使用函數建構子的時候,就已經被 JavaScript 給準備好了,所以我們可以試著輸入 john.proto,我們會得到一個物件。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

那這個物件是什麼?我們先前有講過函數就是物件,在 JavaScript 函數就是物件,然後所有物件都有原型屬性 (prototype property),但是一般來講我們並不會用到,他只是掛在那裏而已。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

function Person(first, last) { this.firstname = first; this.lastname = last; } Person.prototype.getFullName = function() { return this.firstname + ' ' + this.lastname; } var john = new Person('John', 'Doe'); console.log(john);

我們可以看到我們可以原型下有了 getFullName(),換一個角度來看。

所以我們可以知道,當然函數被當作函數建構子使用時,就會有原型,那為什麼我們要將 getFullName 放進原型內?我們也可以直接放在函數建構子內?

function Person(first, last) { this.firstname = first; this.lastname = last; this.getFullName = function() { return this.firstname + ' ' + this.lastname; } } var john = new Person('John', 'Doe'); console.log(john);

假設今天有一千種物件都放在 getFullName 內,就會佔據比較多的記憶體,但若放在原型上就會比較好。

以效能來講將重複的屬性與方法放在原型上會比較好,因為這樣做我們就不用每一次都在 Person 裡面加入物件,只需要將重複需要使用的放在原型,然後透過原型去取得使用就好了。

所以就可以這樣寫

function Person(first, last) { this.firstname = first; this.lastname = last; } Person.prototype.getFullName = function() { return this.firstname + ' ' + this.lastname; } Person.prototype.getFormaFullName = function() { return this.firstname + ', ' + this.lastname; } var john = new Person('John', 'Doe'); console.log(john); var jack = new Person('John', 'Doe'); console.log(jack);

危險小叮嚀:「new」與函數

前言

這章節是關於「new」與函數的危險小叮嚀,在前面我們有大致上講過,當加上 new 時,會建立空的物件,然後 this 會指向空的物件,如果你不回傳任何東西的話,它會回傳新物件,但這邊就是函數危險的地方。

「new」與函數

如果今天我們忘記加上 new 會發生什麼事情呢?

function Person(first, last) { console.log(this); this.firstname = first; this.lastname = last; } var john = Person('John', 'Doe'); console.log(john);

第一個我們可以發現 this 指向了 window,然後 JavaScript 並不知道我們要對這個函數執行,對他來講只是一個函數,也因為這樣子所以他只會回傳 undefined。

那這樣會有甚麼問題?當我們取用 proto 就會出現錯誤。

function Person(first, last) { console.log(this); this.firstname = first; this.lastname = last; } Person.prototype.getFullName = function() { return this.firstname + ' ' + this.lastname; } var john = Person('John', 'Doe'); console.log(john.getFullName());

這個錯誤是因為 john 是一個 undefined,所以無法取得方法。

通常這都是大家認為的函數建構子缺點,但如果我們遵照一些規範來設計的話,就可以減少一些問題,例如任何我們要作為函數建構子的函數首字
母寫成大寫,第一個字母大寫,這樣子會比較方便於 debug,而這是一個傳統方式。

任何我們要作為函數建構子的函數,永遠都要第一個字母大寫作為建構子名稱。

觀念小叮嚀:內建的函數建構子

前言

我們對於函數建構子已經有一個基本瞭解了,所以這邊就可以來討論一下內建的函數建構子。

內建的函數建構子

這邊將會開始講一下已經存在於 JavaScript 的內建函數建構子,所以我們可以看看內建的函數建構子有什麼,或許我們已經用了許多次,但不一定瞭解它。

來試著撰寫程式碼瞭解吧。

var a = new Number(3);

首先這是一個函數,一個函數建構子,用了首字母大寫的傳統。

我們可以看到輸出的是一個物件,為什麼?函數建構子會建立什麼?物件。

所以我們可以試試看我們所瞭解的內建建構子。

var a = new Number(3); var b = new String('Hellow'); var c = new Object({firstname: 'jack'});

那函數建構子本身有原型,所以我們可以查看原型,而它內建一些方法。

var a = new Number(3); Number.prototype

所以我們可以這樣使用

var a = new Number(3); a.toFixed(2)

另外小提一下 b,並不是字串,也不是純值。

var b = new String('Hellow');

它是一個物件,所以我們才能夠這樣子使用 indexOf()

var b = new String('Hellow'); b.indexOf('e');

所以這些內建的函數建構子看起來很像是幫我們建立字串,但其實是物件,且包含了字串。

那其實下面這個做法

"Hello".length;

等於這個樣子

new String("Hello");

所以說下次若我們看到 new,例如 new Date

var a = new Date('2019/05/23');

我們其實是取得了一個物件,然後使用了 JavaScript 內建提供的方法。

而這些方法實際是存在於 Date。

那如果今天我們要自己加入另一個原型呢?

String.prototype.isLengthGreaterThan = function (limit) { return this.length > limit; } console.log('jack'.isLengthGreaterThan(3));

那這時候所有字串就都可以使用 isLengthGreaterThan 這個方法,而這就是原型繼承的強大,許多框架都會這樣做,但是撰寫時要注意不要複寫到原本提供的建構子。

字串可以這樣做,那數字呢?

String.prototype.isLengthGreaterThan = function (limit) { return this.length > limit; } console.log('jack'.isLengthGreaterThan(3)); Number.prototype.isPositive = function () { return this.length > 0; } console.log(10.isPositive());

得到了一個錯誤,為什麼?

雖然 JavaScript 會將字串轉換為物件,但數字並不會。

但是可以這樣寫

Number.prototype.isPositive = function () { return this.length > 0; } var a = new Number(3); console.log(a.isPositive());

危險小叮嚀:內建的函數建構子

前言

在前面章節已經談過了內建的函數建構子,但是他也是有一些危險的地方,以及他們很簡潔,但也有很危險的地方,而這章節主要會講解內建函數建構子在處理純質、布林、數值及字串時很危險。

內建的函數建構子

先來看看看範例

var a = 3; var b = new Number(3); a == b;

我們可以看到是 true,基本上感覺沒什麼問題,當如果用強制辨別型別呢?

var a = 3; var b = new Number(3); a === b;

答案會是 false,原因是透過內建建構子所生成的 b 是一個物件。那 a 呢?

是一個純值,所以在雙等號狀況下 JavaScript 被轉換成了兩者相同的型別,所以在三等號狀況下就會出現 false,所以我們可以知道透過內建函數建構子所建立的純值並不是一個真正的純值,所以得這邊得到一個結論。

一般來講不要使用內建函數建構子比較好,使用實體語法就好。

如果真的需要使用到內建函數建構子,自己就必須知道在做什麼,舉凡型別的差異。

但是有些狀況不太一樣

var a = Number('3'); typeof(a);

在某些狀況下用上面這種寫法是可以的,但是要清楚知道有無 new 的差異。

new 就是在建立物件後呼叫函數得到物件。

moment.js

一般來講我們都是使用 Date 來處理時間函數,所以課堂上推薦了一個不錯的套件 moment.js,可以看到裡面有許多函數來做處理日期及日期運算,課程上也有講如果我們需要使用到大量的日期,就會推薦使用這個套件,避免踩到一些不必要的雷,而不是使用內建函數建構子 (new Date)。

危險小叮嚀:陣列與for in

前言

前面已經瞭解了使用內建函數建構子的某些危險地方,但其實陣列與for in,也是有一些危險的地方。

陣列與for in

在前面幾個章節有講過陣列就是物件,所以我們一樣可以使用 for in來做處理。

var arr = ['John', 'Jane', 'Jim']; for (var prop in arr) { console.log(prop + ': ' + arr[prop]); }

JavaScript 的陣列與其他程式語言有點不太一樣,prop 其實是名稱而 arr[prop] 是值的配對,而這就像名稱/值的配對,所以我們可以透過中括號取的,有點模糊吧,試著寫出物件會比較清楚。

var obj = { 0: 'John', 1: 'Jane', 2: 'Jim', } obj[0];

而這代表著有一些問題,課堂上是著在原型上加入一個原型。

Array.prototype.myCustomFeature = 'cool!'; var arr = ['John', 'Jane', 'Jim']; for (var prop in arr) { console.log(prop + ': ' + arr[prop]); }

所以一般來講都會避免在 JavaScript 中使用 for in,因為陣列就是物件。

Object.create與純粹的原型繼承

  • PS.這邊與 new 建立的原型差別在於, Object.create 所參考的物件就是原型,在該物件加上的方法,就是直接在原型上加上方法,而以 new 產生原型鍊參考的物件本身並不是原型。

前言

前面我們已經瞭解使用函數建構子建立物件的方式,我們也看過函數建構子的出現原因是為了模仿其他不能實作原型繼承的程式語言,所以這邊會有點尷尬,在其他的程式語言是使用 class 來定義物件該做什麼,然後再用 new 來建立物件,而這也就是函數建構子想模仿的事情。

而有些人在開發時都會比較專注於原型繼承,而非古典繼承。

另外 JavaScript 還有一個建立物件的方式,在較新的瀏覽器都會內建就是 Object.create,所以來看一下範例順便想想原型繼承吧。

Object.create

首先我們用物件實體語法建立一個新物件

var person = { firstname: 'Default', lastname: 'Default', greet: function() { return 'Hi' + this.firstname; } }

首先這邊要注意一件事情,物件不會建立新的執行環境,所當不寫上 this 時,他就會直接去找全域環境。

這邊要注意一件事情,接下來的寫法都是在做同一件事情 [ 建立物件 ],而這是使用了現代瀏覽器內建的方法來建立新物件

var person = { firstname: 'Default', lastname: 'Default', greet: function() { return 'Hi' + this.firstname; } } var john = Object.create(person); // 傳入person console.log(john);

可以發現是可以獲取到物件內的東西,但是為什麼會是空物件呢?在 PJCHENder 文章中有提到

透過 Object.create() 可以建立一個空物件,同時可以將你帶入Object.create() 的參數內容變成該物件的原型。

所以原因是出在使用 Object.create() 這個語法是在建立一個空的物件,然後將傳入的參數變成該物件的原型。

而使用這個方式我們可以彈性的複寫 or 調整屬性及方法。

var person = { firstname: 'Default', lastname: 'Default', greet: function() { return 'Hi' + this.firstname; } } var john = Object.create(person); // 傳入person console.log(john); console.log(john.firstname); john.firstname = 'John'; console.log('----'); console.log(john.firstname);

而這個好處我們可以保留原型並使用原型。

var person = { firstname: 'Default', lastname: 'Default', greet: function() { return 'Hi' + this.firstname; } } var john = Object.create(person); // 傳入person console.log(john); console.log(john.firstname); john.firstname = 'John'; console.log('----'); console.log(john.firstname); console.log(john.greet());

而這也就是本章節在講的純粹的原型繼承。

所以我們可以透過 Object.create() 來覆寫、隱藏屬性及方法,我們只要對 person 新增屬性及方法,然後透過使用 Object.create() 讓它也擁有這些屬性及方法,而這就是純粹的原型繼承。

專案過舊不支援 Object.create()

但是這邊要注意一件事情 Object.create() 僅有在較新的瀏覽器上才會支援,所以若專案需要在比較舊的瀏覽器或環境,那就不能用這種方式,而是改使用 polyfill。

polyfill 是將 JavaScript 引擎缺少功能,增加功能到程式中,所以無論你在處理的程式如何,可以用這個方法去檢查引擎有沒有這個功能並增加功能。

首先今天的狀況是瀏覽器過舊沒有支援 Object.create(),那就可以這樣做。

if (!Object.create()) { Object.create = function (o) { if (arguments.length > 1) { throw new Error('Object.create implementation only accepts the first parameter.'); } function F() {} F.prototype = o; return new F(); } }

來解釋一下程式碼,首先我們使用 if (!Object.create()) 判斷瀏覽器是否有支援 Object.create(),若沒有就會回傳一個 undefined,在前面加入一個 ! 來反轉運算式結果,將 undefined 轉換成布林值,而 undefined 的布林值為 false,所以這一段的意思就是,當「Object.create()」不存在的時候就執行裡面的程式碼。

接下來 if (arguments.length > 1) 意思是當傳入的參數超過一個以上就會回傳錯誤訊息「Object.create implementation only accepts the first parameter.」 > Object.create() 只接受一個參數。

最後一行就是 function F() {} 會建立一個空的物件,然後將傳入的參數內容放入到 F.prototype 中,最後再用函數建構子的方式回傳 return new F();,這樣就可以達到原本的 Object.create() 效果。

ES6與類別

前言

這章節主要講解 ES6(JavaScript EcmaScript 2015 或 EcmaScript 6) 的部分,而這章節將講解 ES6 另一種建立物件與設定原型的方法。

class 在其他程式語言是非常受歡迎的,可以用來定義物件的方法、屬性等該做什麼,但前面有講過 JavaScript 本身並沒有類別 (class)。

ES6與類別

那類別看起來像什麼?

class Person { constructor (firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } greet() { return 'Hi' + this.firstname; } } var john = new Person('John', 'Doe'); console.log(john); console.log(john.greet());

基本上與原本在寫函數建構子一樣,所以做法完全沒有太大差異。

但是這邊要注意一件事情 JavaScript 的 class 與其他程式語言不同,用 class 建立的內容它還是一個物件。

class Person 其實就是在建立一個物件。

如何設定原型?

class Person { constructor (firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } greet() { return 'Hi' + this.firstname; } } class InformalPerson extends Person { constructor (firstname, lastname) { super(firstname, firstname); } greet() { return 'Yo!' + this.firstname; } } var john = new Person('John', 'Doe'); console.log(john); console.log(john.greet());

透過 extends 可以將 InformalPerson 設定為 Perosn 的原型。

tags: WierdJavascript