【克服 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,這可以幫助開發者迴避一些因建構子產生的問題。
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 的建構式參數設定。