這篇筆記跟function有關的重要概念。
函式的屬性和方法
在JavaScript語法裡,函式是一種物件,可以有自己的屬性和方法:
property
- name
method
- call
- apply
- bind
First-Class Function
JavaScript的函式是First-Class Function,意即函式是First-Class Citizen (一等公民),所謂的一等公民有以下特色:
- 可被儲存成變數或物件屬性
- 可當作引數輸入至任何函式
- 可被任何函式回傳
- 可有方法(method)
回顧JavaScipt的函式確實是可以儲存成變數或是當作物件屬性(即:方法 method);也能被當成引數傳入函式(即:廻呼函式 callback function);也能被任何函式回傳;當然也可以有自己的方法,像是bind、call、apply。
Higher-Order Function
Higher-order function的概念其實不難,以數學符號 f( g( . ) ) 來看, f( ) 就是 higher-order function。
在JavaScript的語法裡,只要函式有以下其中一點就可以被稱作是higher-order function:
- 接受函式當作引數(e.g., callback function)
- 回傳函式(e.g., closure)
以廻呼函式(callback function)為例,廻呼函式(callback function)的定義是「函式傳入另一個函式作為引數並且晚一點才會被呼叫」。一般來說,將程式碼封裝成廻呼函式有幾個好處:
- 分離程式碼(split code),並且讓分離出來的程式碼可被重複利用、
- 抽象化 (abstraction)
所謂抽象化是指,在使用函式時只要知道函式的功能和用途而不需要知道函式內部程式碼怎麼寫。廻呼函式程式碼通常稱為「low-level abstraction」,而接受廻呼函式的函式程式碼則稱為「higher-level abstraction」。
IIFE (Immediately Invoked Function Expression) 立即函式
IIFE並不是JavaScript的其中一種語法,IIFE其實是過去開發者想出來的模式(pattern),用於函式只需呼叫一次的時候:
( function () {
console.log(“A function called once”);
} )( );
IIFE的第一個 ()
是要告訴JavaScript這個匿名函式(anonymous function)是一則運算式(expression),若不加第一個括號會報錯;而第二個 ( )
其實是函式呼叫運算子(call operator),讓函式可以立刻被呼叫。
IIFE 為什麼會被設計出來?主要是因為在ES2015語法出來以前,開發者們為了避免只有function scope的變數被污染,因此想出IIFE 模式,將變數封裝(encapsulation)在這個立即函式裡面。不過當ES2015語法出現 let、const和block scope之後,IIFE的功能可以被 { … }
區塊來取代。
Default Parameter 預設參數 (ES2015)
預設參數是ES2015 的新語法,過去是以circuit方式用 ||
OR運算子來設定預設參數,現在則是可以用新語法來完成:
// Before ES2015
function setName (name) {
return name || ‘anonymous’;
}
// ES2015
function setName (name = ‘anonymous’ ) {
return name;
}
以下列出幾個有關預設參數的重點:
- 預設參數的值可為前面預設參數的值,例如:
function booking (buyer = 1, price, total = buyer * price)
- 原則上函式參數輸入值是要依序輸入
- 可以用
undefined
跳過輸入參數值,直接使用用預設參數值,例如:booking(undefined, 250, undefined);
argument 引數
- JavaScript 只有 pass by value,沒有 pass by reference
- 不管是primitive type或是reference type都是以call by value的方式傳入函式,但傳入後的差別在於:
- primitive type:複製變數值,若函式內修改引數的值不會更動到原始變數的值(因為是用複製的方式傳值);
- reference type:複製物件變數所在記憶體位址,若函式內修改物件引數的屬性值,就會更動到原始的物件值(因為複製的是原始物件的記憶體位址)
call, apply, bind
現在回頭來看函式的三個方法call、apply、bind。
前一篇有提到this會隨著所在環境的不同而有所不同,所以有些時候需要使用這個方法去綁定函式中所指稱的this。
以下範例showName函式的this是undefined,所以用call、apply或bind綁定函式中的this:
const john = {
firstName: ‘John’,
}
function showName( firstName, lastName ) {
console.log(this.firstName + “ ” + lastName)
}
showName.call(john, “Woods”); // John Woods
showName.apply(john, [“Woods”]);
showName.call(john, …[“Woods”]);
const showWoodsName = showName.bind(null, “Woods”);
showWoodsName(john); // John Woods
call和apply都是立即執行函式,兩者差別在於apply方法傳入的引數必須是陣列。不過在語法ES2015以後,陣列可以用展開運算子(spread operator) …
去展開、拆解引數陣列,所以apply的功能其實是可以被call替代。
而bind則是會回傳一個新的函式,範例裡先綁定 ”Woods”
傳回新函式(這種綁定部分參數的作法稱作partial application),並且用 null
跳過要傳入的物件,然後才在新函式 showWoodsName
傳入 john
才執行。
Closure 閉包
最後要談一個很重要的概念,closure,中文稱「閉包」。以前也有寫過一篇專門文章,但有些地方還是沒有寫得很清楚,所以重新整理在這篇文章。
先直接列出幾個關於closure的重點:
- Closure指的是任何一個函式可以記住所在環境(context)的變數
- Closure其實是函式會自動產生的一個內部屬性(internal property),可以用瀏覽器開發者工具查看
[[ scopes ]]
,這種內部屬性一般是沒辦法用程式去直接存取的,只能透過函式去間接存取。
function counter () {
let count = 0;
return function () {
return ++count;
}
}
console.log ( counter() ); // 1
console.log ( counter() ); // 2
更詳細地說,JavaScript函式會透過範疇鏈(scope chain)記住所在執行環境的環境變數,並儲存在closure裡。
此外JavaScript執行函式時,若在函式內部找不到變數,會優先查看函式的closure,找不到才會去從scope chain去找變數,譬如:
```
const a = 2;
function outer () {
let a = 5;
return function () {
return a * 10
}
}
const inner = outer();
console.log( inner() ); // 5