【克服 JS 】第六章 建立物件

JavaScript 全攻略:克服 JS 的奇怪部分,第六章 建立物件,學習紀錄。

Udemy 課程連結:

JavaScript 全攻略:克服 JS 的奇怪部分

6-1 函式建構子、「new」與 JavaScript 的歷史

JavaScript 剛被創造出來時,為了吸引 JAVA 開發者借鑒了不少東西,包含名子 JavaScript,而在物件部分,向 JAVA 和 C++ 借鑒了 new 這個關鍵字,new 和物件實體與法一樣,都可以讓使用者快速建立物件,與之一起出現的用法就是函式建構式(function constructor)。

函式建構式(function constructor):用來建立物件用的一般函式。

參考範例:

function Person() {
  this.firstname = 'John'
  this.lastname = 'Doe'
}

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

// Person {firstname: "John", lastname: "Doe"}

使用 new 後面接一個函數時,會建立一個空物件,接著執行這個函式,並將函式執行環境的 this 指向這個空物件,利用 this 來增加物件的屬性,最後不需使用 return,JavaScript 會自動回傳這個物件,所以我們可以得到 john 是一個設定了兩個屬性的物件。

驗証一下函式是否真的有執行,執行時 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)

// Person {}
// This function is invoked.

// Person {firstname: "John", lastname: "Doe"}

一開始的 this 為一個空物件,函式真的有被執行。

這個用來建立物件的函式,就叫做函式建構子,使用這個函式建構子就可以重覆建立相同的物件。

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

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

// Person {firstname: "John", lastname: "Doe"}

// Person {firstname: "John", lastname: "Doe"}

想要每次建立的物件有不同的屬性值的話,可以在函式建構子加上輸入值:

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 {firstname: "John", lastname: "Doe"}

// Person {firstname: "Jane", lastname: "Doe"}

6-2 函式建構子與「.prototype」

函式就是物件,它有一些隱藏屬性,其中有個在用函式建構子才會用到屬性 prototype

取用物件原型的內建屬性是 __proto__ ,而不是這裡的 prototype,函式的內建屬性prototype 不是用來取用函式自己的原型物件,而是用來取用建構子創造出來的物件原型。

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.getFullName())

var jane = new Person('Jane', 'Doe')
console.log(jane.getFullName()) 

// John Doe

// John Doe

使用函式建構子的屬性 prototype 在原型新增一個函式 getFullName。使用函式建構子建立的物件都有 getFullName 可以用。

在物件已建立後再使用 prototype 新增的函式,該物件一樣可以取用:

function Person(firstname, lastname) {
  this.firstname = firstname
  this.lastname = lastname
}

var john = new Person('John', 'Doe')

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

console.log(john.getFullName())

// John Doe

如果在函式建構子裡就新增成員函式,例如:

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

var john = new Person('John', 'Doe')

console.log(john.getFullName())

// John Doe

一樣可以使用,但每個建立的物件裡都會再建立一次這個成員函式,如果建立很多物件,就會增加記憶體,如果使用 prototype 來建立物件成員函式的話,這個函式只會在原型物件上建立一次每個用函式建構子建立的物件,都可以用原型取得這個函式,不需要每個物件裡都加上這個函式,以節省記憶體。

6-3 危險小叮嚀:「new」與函式

在使用函式建構子建立物件時,如果忘了加上 new

var john = Person('John', 'Doe')

函式一樣會執行,變成一般的呼叫函式但不會回傳物件,john 會變成 undefined,執行時不會在這行顯示有錯誤。

良好的習慣是作為函式建構子的函式,會將首字母大寫,如果有一堆錯誤的話,比較容易看到,注意到這個首字母大寫的函式沒有 new 然後知道忘記加上 new 關鍵字。

6-4 內建的函式建構子

JavaScript 已內建了一些函式建構子,例如 Number()String()等等。

var a = new Number(3)

在變數 a 指向的東西,並不是基本型別 Primitive Types,而是物件型別 Object Type,在這個物件裏頭,有一個基本型別數值 3 的存在。

因為這是物件,它有原型,所以有 Number.prototype,所有Number 物件都可以取用到。

var b = new String('John')

b 不是一個單純的字串,而是一個字串物件。

所以當我使用這些函數建構子,要記得是在建立物件。

所以如果我們想要對所有的字串加上一個成員函式時,可以像這樣:

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

console.log('John'.isLenghtGreaterThan(3))
// true

String.prototype 加上成員函式後,所有的字串都可以直接呼叫這個函式來用了。

許多資源庫和框架都是這樣新增加概念和功能。

如果是用在數字上的話,例如:

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

console.log(3.isPositive())
// 顯示錯誤: Invalid or unexpected token

雖然 JavaScript 會轉換字串,但它不會自動轉換數值為物件,所以最好不要直接對看起來像是純值的東西取用成員函式。

6-5 危險小叮嚀:內建的函式建構子

內建函數建構子在處理純值,尤其是布林、數值、字串時,很危險。

看起來像純值,但不是純值,可能會因此出現非預期的結果,例如:

var a = 3
var b = new Number(3)
console.log(a==b)  // true
console.log(a===b) // false

a == b true 為什麼?,因為它轉型了。

內建的函式建構子,他建立的純值不是真正的純值,在進行比較時會出現問題,一般來說不使用函式建構子,用實體語法比較好。

另外,如果有用 JavaScript 處理時間的需求,作者不建議使用內建的時間方法,他推薦使用 JavaScript 函示庫 moment.js,這可以幫助開發者迴避一些因建構子產生的問題。

moment.js

6-6 危險小叮嚀:陣列與 for in

for in 是用來跑一遍物件中所有名稱/值的迴圈,但也可以使用 for in 來跑一遍陣列中所有值,例如:

var arr = ['John', 'Jane', 'Jim']

for(var prop in arr) {
  console.log(prop + ': ' + arr[prop])
}
// 0: John
// 1: Jane
// 2: Jim

因為 arr 實際上也是個物件,陣列裡的值就是物件的屬性

假如載入了其他程式的關係改寫了 Array.prototype,例如:

Array.prototype.myCustomFeature = 'cool!'

var arr = ['John', 'Jane', 'Jim']

for(var prop in arr) {
  console.log(prop + ': ' + arr[prop])
}
// 0: John
// 1: Jane
// 2: Jim
// myCustomFeature: cool!

會產生非預期的結果。

所以要跑一遍陣列最好還是使用一般 for 迴圈比較安全:

Array.prototype.myCustomFeature = 'cool!'

var arr = ['John', 'Jane', 'Jim']

for(var i = 0; i < arr.length; i++) {
  console.log(prop + ': ' + arr[prop])
}
// 0: John
// 1: Jane
// 2: Jim

6-7 Object.create 與純粹的原型繼承

使用函式建構子建立物件的語法,其實是在模仿其他語使用 new Class 建立物件的語法。

在 JavaScript 還有一種純粹的原型繼承來建立物件的方式 Object.create,看看以下範例:

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

var john = Object.create(person)

console.log(john) // Object {}
console.log(john.greet()) // Hi Default

john.firstname = 'John'
john.lastname = 'Doe'

console.log(john) // Object {firstname: "John", lastname: "Doe"}
console.log(john.greet()) // Hi John

Object.create() 這個方法建立物件,傳入給它的參數會成為創建出物件的原型。

Object.creat 是 ES5 以後新增的語法,某些舊版瀏覽器並不支援,這時可以用 polyfill 來處理。

polyfill:把執行環境可能缺少的程式補上去。

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 不存在,就將一個函式表示式(並傳值 o)賦予給 Object.create。若賦予 Object.create 的函式傳入參數超過 1 個以上,就報錯。

接著設定空函式 F,將 o 傳入 F 的 prototype,最後 if 陳述句回傳一個物件,這個物件是用 new 函數建構子建立,原型為函式 F 的物件。

6-8 ES6 與類別

ES6 新增的類別(class),類別是在其他程式語言中普遍使用的方式。

使用方法像這樣:

class Person {

  constructor(firstname, lastname) {
    this.firstname = firstname
    this.lastname = lastname
  }

  greet() {
    return 'Hi ' + firstname
  }
}

var john = new Person('John', 'Doe')

設定一個 class 類別 Person,在裏頭有 constructor 建構子,和函式建構子一樣,我們可以預先設定物件值,也可以設定函式方法在 class 裏頭。

從其他語言過來的人會覺得有類別能用很棒,但要注意的是,其他語言的類別,不是物件,只是一個定義,而 JavaScript 中的類別,就是一個物件,所以其實是在用物件來建立物件。

類別可以使用 ES6 新增的 extends,來設定物件原型:

class InformalPerson extends Person {

  constructor(firstname, lastname) {
    super(firstname, lastname)
  }

  greet() {
    return 'Yo ' + firstname
  }
}

設定類別 InformalPerson 並 extends Person(extends會繼承類別 Person),在 constructor 內,透過 super 方法去獲得父類別 Person 的建構式參數設定。

參考資料:

JavaScript 基礎二三事|2018 iT 邦幫忙鐵人賽

那克斯的學習筆記

TOP