[課程筆記]克服JS的奇怪部分-CH5 建立物件

Udemy課程連結
課程總目錄

本篇筆記目錄:

U1 函數建構子、「new」與 JavaScript 的歷史
U2 函數建構子與「.prototype」
危險小叮嚀:「new 」與函數
觀念小叮嚀:內建的函數建構子
危險小叮嚀:內建的函數建構子
危險小叮嚀:陣列與 for in
U3 Object.create 與純粹的原型繼承
U4 ES6 與類別

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

我們已經看過一種建立物件的方法,像物件實體語法,然而,有一些其他方法可以建立物件,尤其是遇到要設定原型時

物件實體語法複習

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

用 New 建立物件

function Person(){
	console.log(this);
	this.firstname='john';
	this.lastname='Doe';
	console.log("this function is invoke");	
}

//用 new 建立物件
var john= new Person();
console.log(john);

//Person{}
//this function is invoke
//Person{firstname:john,lasname:'Doe'}

new 是個運算子,使用 new 時一個空物件被建立(像var a={}
執行過程:

  1. 一個空物件被建立
  2. 呼叫後面的函數
  3. new 關鍵字會改變 this 變數指向的東西,this 變數會指向新的空物件
  4. 當我們增加 .firstname.lastname ,增加到空物件上。
  5. 當函數結束執行時,該物件會被函數自動回傳
  6. 但如果我們什麼都不對 this 做的話(console.log(john)),他將會回傳本身(Person{})。

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

function Person(){
	console.log(this);
	this.firstname='john';
	this.lastname='Doe';
	return{greeting:'I got in the way'};
}


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

//Object{greeting:'I got in the way'}

如果我們想要用同樣的屬性和方法建立更多人呢?

function Person(){
	console.log(this);
	this.firstname='john';
	this.lastname='Doe';
	console.log(this function is invoke)	
}

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

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

//Person{}
//this function is invoked
//Person{firstname:"John",lastname:"Doe"}

//Person{}
//this function is invoked
//Person{firstname:"John",lastname:"Doe"}

不過有點問題,firstnamelastname應該有他們自己的值,我們可以用「函數建構子」來改良。

函數建構子 function constructor

一個正常的函數用來建立物件

函數建構子是被用來增加新物件的屬性和方法,他能幫我們設定新屬性和新方法

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"}

U2 函數建構子與「.prototype」

Prototype

Prototype 是用函數建構子建立的物件其原型鏈所指向的東西。

當我們使用函數建構子,它已經幫我們設定好原型了

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

var john= new Person('John','Doe');
var jane =new Person('Jane','Doe');

console.log(john.__proto__);

//Person{}

JavaScript 所有的函數都有原型屬性(prototype property),從它是空物件就有。除非你將函數作為函數建構子來使用,不然他永遠不會用到,但一旦你用 new 運算子呼叫函數,他就有意義了。

我們看看這個範例:

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{firstname:"John",Lastname:"Doe",getFullName:function}
//Person{firstname:"John",Lastname:"Doe",getFullName:function}


john 指向 person.prototype 做為原型(__proto__),jane 也是。當我們新增getFullName給原型(加上Person.prototype.getFullName)後會發現,JohnJane 都有了getFullName() ,所以如果我們輸入john.getFullName(),會先在john尋找getFullName(),找不到後到原型去找,而john的原型指向Person.proptotype,最後回傳John Doe

這表示任何用Person建立的物件,我們都可以藉由.prototype這個函數建構子的屬性,新增屬性到所有這些物件,如果我用new person建立1000個不同的物件 我可以讓他們全都取用到方法,即使他們已經被建立也一樣

在好的JavaScript程式碼中,屬性在函數建構子中被設定 因為他們常常是不同的值,但方法會位於原型裡

為什麼方法不放入建構子呢?因為方法(函數)就是物件,會佔用記憶體空間,任何我們增加們的東西都會佔據記憶體空間,但如果我們增加到原型,雖然我有一千個物件,但我只有一個方法。


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

當我們用函數建構子,他們也只是一般的函數,會在加上 new 時會建立空物件,this 變數指向空物件,如果不回傳任何東西的話,它會回傳新物件,但這仍然是個函數,這也就是危險的部分!

如果忘記放上 new 關鍵字會發生什麼事呢?

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


var john= Person('John','Doe');
console.log(john.firstname);

// Cannot read property 'firstname' of undefined


JavaScript引擎不知道你要他執行函數,或是用 new 關鍵字執行函數,所以它會讓你正常執行他,而因為他不回傳任何東西,它會回傳undefined,表示你的正在建立的物件會被設定為undefined,當我們要取用屬性或方法時,會因為它不是物件導致錯誤

簡單不會出錯的傳統習慣

任何我們要作為函數建構子的函數,我們會將首字母大寫(Person),如果我有一堆錯誤的話,我比較容易看到

JavaScript有一些新的建立物件的方法,所以函數建構子可能會逐漸消失,不過也不會完全消失,因為我們仍然需要支援已經被寫好的JavaScript程式,作為程式設計師,沒辦法永遠都接觸新的專案,很多時間你要維護舊的專案,所以你要處理這些模式,瞭解這個對你很有幫助,他們只是建立物件的方法,但要確認當你用函數建構子時你有加上new運算子!


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

JavaScript引擎裡面有很多內建的函數建構子

Number

var a= Number(3);

a 不是純值,而是物件。它有原型,所以有 Number.prototype,所有 Number 物件都可以取用到,有些內建方法如to PrecisiontoFixed,所以我們可以輸入a.toFixed(2)

String

var a= new String("John")

a 可以取用到一堆能夠在字串上用的方法,像是indexOf

為了加深觀念,我們輸入 String 會發現它不是字串,是一個物件,裡面有很多屬性和方法

在某些例子中JavaScript知道你要物件而不是純值,所以我可以

"John".length
//4

這個純值就是個值而已,他沒有屬性或方法,但 JavaScript 把它放入有許多屬性和方法的字串物件中,再讓你自動取用他,基本上等於new String('John'),然後再對他做.length

所以有時候JavaScript引擎會把純值(John)包在物件中給你使用 這樣你可以取用你可能需要的屬性或方法

Date

var a = new Date(3/1/2015)

當我們使用 Date 時,所有的屬性和方法是在誰那裡?他們不是在a上,它們都在Date.prototype

我們可以嘗試新增功能

當我們新增一個功能 isLengthGreaterThan

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

console.log("John".isLengthGreaterThan(3))

這個純值字串(John),被字串原型函數轉換成字串物件,this 指向空物件

如果我們在數值上使用呢?

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

console.log(3.isPositive)
//error

我們會得到 error,因為雖然JavaScript會轉換字串,但它不會自動轉換數值為物件
不過我們當然可以這麼使用

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

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

用內建的函數建構子建立純值是很危險的

前面討論過內建的函數建構子以及他們很簡潔,但也危險,這邊有個簡單例子,告訴你為何內建函數建構子在處理純值 尤其是布林、數值、字串時很危險

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

看出危險的地方了嗎?內建的函數建構子他建立的純值不是真正的純值

Date 資源庫

如果你在處理日期,或用內建的Date函數建構子,有個很棒的資源庫叫作 Moment.js,裡面有一堆函數來處理日期 甚至做日期運算,如果你需要用到大量的日期,我建議你用這個資源庫,不要用JavaScript內建的建構子,這能解決一些內建的建構子的問題

用內建的函數建構子建立純值是很危險的,我可以用他們來轉換型別

var c =  Number(3)

這時候我只是用他作為一般函數,沒有 new 關鍵字,但要知道 new 關鍵字的有無會有差異,Number(3)是在呼叫函數,加上 new 是在建立物件


危險小叮嚀:陣列與 for in

因為陣列是物件,所以我可們以把它當物件處例

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

陣列是物件,每個陣列成員都是名稱/值配對,每個成員都成為新的屬性,在這個陣列我新增三個屬性,表示會有點問題,假設我增加功能-.myCustomFeature到我們的陣列,

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!

因為['John','Jane','Jim']這個物件實體呼叫new Array,var arr=['John','Jane','Jim']只是個另一種快速呼叫的方式,所以它的原型會指向 Array.prototype 物件的原型。

用標準的for迴圈,很安全,但遍歷所有屬性不安全,因為陣列是物件,你可能會遍歷到它的原型,所以一般來說在JavaScript會避免這樣做,因為陣列就是物件,


U3 Object.create 與純粹的原型繼承

我們知道函數建構子是為了模仿其他不能實作原型繼承的程式語言而設計

但有另一個建立物件的方法 這沒有模仿別的程式語言,而且較新的瀏覽器都會內建稱為 Object.create

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

}

var john = Object.create(person);
console.log(john);
//Object{firstname:Default}

這邊要注意,因為物件不會建立新的執行環境,所以當我們不寫上 this 時,他就會直接去找全域環境,它不會在那邊找到firstname,因為這在 Person 物件裡,

Object.create 會用它的原型建立空物件,john 的原型是 person 物件,如果我用john.greet 會得到預設值,如果我想要隱藏這些預設值 我只要建立同樣名稱的屬性或方法到新物件

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

var john = Object.create(person);
john.firstname=''John';
john.lastname='Doe';
console.log(john);

//Object{firstname:John...}

如果我們的專案需要支援較舊的瀏覽器,或比較舊的環境,JavaScript 引擎無法支援Object.create的話,我們可以使用polyfill

polyfill

polyfill 是增加引擎缺少的功能到程式中

我們可以用程式去檢查引擎有沒有這個功能,如果沒有,就幫他寫程式,讓他擁有新型瀏覽器的功能,填補舊引擎所沒有的空缺


U4 ES6 與類別

下一版本的 JavaScript EcmaScript 2015 或是 EcmaScript6,也就是ES6,它有一個新觀念,另一個建立物件和設定原型的方法

類別(class)

類別在別的程式語言很受歡迎,他們是定義物件的方法,定義方法和屬性該做什麼,JavaScript 沒有類別,但在下一版本會有,不過有點不同:

class Person{
	constructor(firstname,lastname){
		this.firstname=firstname;
		this.lastname=lastname;	
	}
	greet(){
		return 'Hi'+firstname;
	}
}

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

JavaScript的類別定義物件,我們有建構子(constructor)就像是函數建構子一樣,預先設定他們的值,所以當我建立一個新的人,我用new關鍵字,我可以傳入firstnamelastname,this關鍵字會被設定為新的值,所以新物件就被創造了。

在別的程式語言類別不是物件,他只是一個定義,很像是模版,但JavaScript不同,雖然它有class關鍵字,但其實 class Person就是個被建立的物件。

要如何設定原型?

class InformalPerson extends Person{
	constuctor(firstname,lastname){
		super(firstname,lastname)'
	}
	greet(){
		return 'Yo'+firstname;
	}
}

我們只需要用extends關鍵字就可以建立InfomalPerson,而 Person 是原型,這會設定所有我用這個類別建立的物件之原型,就像Chrome裡面的__proto__一樣。

我們可以用super關鍵字呼叫原型物件的建構子,我們可以傳入初始值到原型鏈裡,然後我可以覆寫或隱藏,就像之前用Object.create做過的。

這只是另一種語法方式,另一種表達方式,但底層的原理都是一樣的,事實上,如果你到網路上看看類別,你會發現,他們說這是JavaScript的語法糖(syntactic sugar)。

Syntactic sugar 語法糖

表示有很多方法做到一件事,但其實做出來的東西都一樣

我們看過函數建構子和Object.create 其實都在做一樣的事情,JavaScript的類別本質上也是一樣的,他沒有改變JavaScript引擎如何處理物件和原型的方式。