若要有效的處理資料,就一定會用到函式,這篇記錄函式有怎樣的特性與閉包運用概念。
目前有整理到的如下 :
- 函式型別
- 立即函式
- 函式的參數使用,不同的函式會有不同的內建關鍵字,常見的有
arguments
(ES6箭頭函式則無此參數)this
(ES6箭頭函式則無此參數)- 自訂參數
- 閉包
- 範圍鏈
- 函式與提升 (Hoisting)
- 函式與變數利用
- 閉包的私有方法
函式
- 使用
function
來宣告一個函式。 - 具名函式,宣告函式後,給定一個名稱以利後續呼叫。
- 匿名函式,宣告函式後,不須給定名稱,常用於立即函式與變數賦予值上。
- 名稱要給也是可以,但通常不會這麼做。
- ES6中可以使用箭頭函式。
- 雖然型別為
"function"
,但它其實是物件的一種。
1 | /* 具名函式 */ |
函式與型別
通常我們會利用 typeof()
來檢查目前值為哪一種型別,若對函式做檢查,會得到 "function"
,但很多技術文章都說,函式其實也是一種物件,那要怎麼知道它的確是物件的一種呢?
- 利用物件原型方式看子型別為何,會得到
"[object Function]"
。 - 將函式當作物件新增屬性,console.log 可以正確取值。
- 將函式使用
new
這個運算子做為物件,可以在 console.log 的結果中往內層展開並找到新增的屬性。
1 | /* typeof 型別檢查為 funciton */ |
立即函式
- 一般函式需要呼叫它才會執行
函式名稱()
- 立即函式會立刻執行,給定名稱沒有意義
( 名稱(){} )()
( (){} )()
1 | /* 一般函式 */ |
函式與參數
一段函式中,常見的內建參數有以下這些 :
- arguments
- this
- 自訂參數
1 | var a = '全域物件中的變數' |
參數關鍵字 arguments
- 能接收所有傳入的值
- 它為類陣列,也就說它沒有陣列的原型方法可以使用
- 其型別為
"object"
1 | function sum(a, b) { |
參數關鍵字 this
參考自己整理 this筆記,this在不同位置中會指向誰。
this 與簡易呼叫
還記得 this
只看在哪裡被呼叫嗎? 又怎樣算是簡易呼叫(simple call)?
來看以下例子 :
1 | var a = '全域物件' |
為什麼兩個都是指向全域?
fnA()
在全域環境中被呼叫,且函式在全域環境下被定義,這點很容易看出來,就算把this
拿掉,也會尋找外層的變數 a。fnB()
被呼叫後,執行了fnC()
,它明明在fnB()
內被執行,為什麼this
還是指向全域? 沒關係,繼續往下看另一個例子。
1 | var a = '全域物件' |
為什麼執行物件中的兩個函式,結果會不一樣,照理說要指向物件中的屬性 obj.a
吧?
- 物件中的
fnA()
,this
正確指向了該物件 - 物件中的
fnB()
執行後,再執行fnC()
,this
卻指向了全域物件?- 執行
obj.fnA()
,是由obj
呼叫fnA()
- 執行
obj.fnB()
,是由obj
呼叫fnB()
,接著執行fnC()
,fnC()
被誰呼叫? 沒有,它是直接被執行的,所以它的this
指向了全域。 - 若將
fnC()
內的this.a
改為a
,則會因為範圍鏈的關係往外層尋找,就會是'函式內變數'
。
- 執行
再來複習一下 this
與簡易呼叫。
- 簡易呼叫是甚麼?
- 直接呼叫,未透過任何方式取用執行。
- 簡易呼叫為全域物件下的一種?
- 否,是
this
會指向全域,而不是該函式被建立在在全域下
- 否,是
- 那些例子屬於簡易呼叫?
- callback function
- 立即函式
- 原型方法,例如陣列的
forEach
,此為繼承共用的原型方法,屬於直接執行。
那如果要將 fnC()
,正確的指向物件屬性 a呢 ? 來改寫上面的例子,
1 | var a = '全域物件' |
為什麼 fnB()
將 this
賦予給一個變數時,就不會指向全域,而是會指向物件呢?
fnB()
,是由物件obj
所呼叫的,就跟fnA()
一樣,他們的this
都會指向該物件。- 在
fnB()
內宣告一個變數,將指向物件的this
賦予給該變數,fnC()
再利用此變數時,就會正確的指向物件obj
。 - 後面的段落會說明閉包的概念。
this 與DOM
console.dir
可以看到它原本的物件有哪些屬性- 可以使用
this
指向 DOM的單一元素
1 | console.dir('某個標籤元素') |
this 與 call、apply、bind
- call與 apply會立刻執行,bind則需要呼叫。
- 非嚴格模式傳入
null
、undefined
,則會指向全域物件。 this
為物件型別,也就是說傳入後會變成包裹物件(建構式)。- 若傳入的是數字,會變成
Number()
- 若傳入的是字串,會變成
String()
- 若傳入的是數字,會變成
1 | var c = 5 |
嚴格模式
- 加入
use strict
- 不會影響不支援嚴格模式的瀏覽器
- 可依據執行環境設定
use strict
- 透過拋出錯誤的方式消除ㄧ些安靜的錯誤(消除小錯誤)
- 禁止使用ㄧ些有可能被未來版本 ECMAScript定義的語法
- 在這些方法中的
this
,預設值是undefined
,若未傳入值又去呼叫它,就會是undefined
。
1 | function fn(a, b){ |
函式與自訂參數
參數傳入值
- 當函式執行時,只會根據位置傳入值
- 在函式中,參數所運用的地方才會是對應的
1 | function fn(a, b, d, e){ |
參數傳入物件
當利用函式內的程式碼修改傳入的物件時,會依據傳參考特性修改原物件的屬性值,若物件在函式內做了深層複製的話則不會。
參數傳入函式( callback function )
除了可以傳入值、物件、陣列,也可以將另一個函式傳入。
1 | // 函式參數傳入函式 |
閉包
閉包為範圍鏈、記憶體空間、表達式以及函式等觀念總合的概念,重複執行函式時,其變數值就會累加上去。要利用閉包的概念前,先來複習一下函式還有怎樣的特性。
- 範圍鏈
- 函式與提升
- 函式與記憶體空間
函式與範圍鏈
在函式中的變數,若沒有宣告,則往外層尋找直到全域,若全域也沒有,則報錯。
靜態作用域
跟在哪裡被呼叫無關,跟位置有關。
來看以下範例,函式的位置會影響最後的結果。
1 | var a = 1 |
動態作用域
跟在哪裡使用無關,跟在哪裡被呼叫有關,最常見的就是 this
的運用。
函式與提升(Hoisting)
在函式中,
- 將自訂參數重新宣告不會有任何作用
- 函式中的函式,不會提升至外層
- 若在函式中不將變數宣告或者非自訂參數,則可能依據呼叫環境在全域物件中建立變數
1 | function fn(a){ |
函式與記憶體空間
先看下面的例子,函式每次執行時,都會加 1。
1 | var a = 0 |
如果將變數 a,移動到函式內,重複執行函式時,就會因為記憶體空間被釋放掉,又重新宣告而無法累計。
1 | function fn(){ |
接下來,改寫上述範例,假設今天去夜市套圈圈,老闆的每個籃子裡總共有五個圈圈,規則是每次只能丟一個圈圈。今天有兩個玩家,都另開新局,他們都能有五個圈圈可投。
- return 一個函式,此函式包含著變數,該變數會視為還須利用而不會被上層的函式釋放掉
- 分別宣告變數並賦予函式,函式內部所宣告的變數不會互相影響
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function game(){
var times = 5
return function(){
times -= 1
return `圈圈剩餘 ${times} 個`
}
}
var personA = game()
console.log(personA()) // 圈圈剩餘 4個
console.log(personA()) // 圈圈剩餘 3個
console.log(personA()) // 圈圈剩餘 2個
console.log(personA()) // 圈圈剩餘 1個
console.log(personA()) // 圈圈剩餘 0個
var personB = game()
console.log(personB()) // 圈圈剩餘 4個
console.log(personB()) // 圈圈剩餘 3個
console.log(personB()) // 圈圈剩餘 2個
console.log(personB()) // 圈圈剩餘 1個
console.log(personB()) // 圈圈剩餘 0個
閉包進階與函式工廠
這次老闆使用機器人自動丟五個圈圈出去,每次丟一個就紀錄一次剩餘多少個圈圈。
1 | function game(){ |
閉包進階的私有方法
讓我們再度強化機器人的可用性,基礎次數五次,每丟一次就記錄一次到陣列中,利用閉包的原理,將函式 return一個物件。
但是,這台機器人卻有個地方無法正常顯示,該怎麼修復它呢?
1 | function game(){ |
函式 personA
return的物件,其包含許多函式與變數,而單純變數值卻無法修改? 我們來檢查看看,
1 | // 節錄 personA片段 |
可以看到有的直接變成字串,而待在函式裡的變數則維持變數的樣子,讓我們再回想閉包的原理,若該函式內的變數有需要用到,則記憶體空間不會被釋放掉,也就是說,屬性值為單純的變數,被使用後就釋放掉了,待在函式內的則不會。那麼,我們來修正機器人,將 addTimes
這個屬型值改為函式,這樣就能正確顯示了。
1 | function game(){ |