函式(Function)#
JavaScript 函式有兩種定義方式,加上一種簡化語法:
| 寫法 | 語法 | 提升 | this | 說明 |
|---|
| 函式宣告 | function fn() {} | 有 | 有自己的 | 最傳統的定義方式 |
| 函式表達式 | const fn = function() {} | 無 | 有自己的 | 將函式賦值給變數 |
| 箭頭函式 | const fn = () => {} | 無 | 繼承外層 | 函式表達式的簡寫語法 |
箭頭函式本質上是函式表達式的語法糖,但有兩個重要差異:沒有自己的 this,且不能作為建構函式(不能 new)。
函式宣告(Function Declaration)#
最傳統的定義方式,以 function 關鍵字開頭,後面接函式名稱:
1
2
3
4
5
| function add(a, b) {
return a + b;
}
console.log(add(3, 5)); // 8
|
函式宣告有 提升(Hoisting) 特性。JavaScript 在執行前會先掃描整份程式碼,將函式宣告提升到最前面,因此可以在宣告前呼叫:
1
2
3
4
5
| console.log(greet("Alice")); // "Hello, Alice!"(合法,因為宣告被提升了)
function greet(name) {
return `Hello, ${name}!`;
}
|
函式表達式(Function Expression)#
將函式賦值給一個變數,函式本身可以匿名:
1
2
3
4
5
| const multiply = function(a, b) {
return a * b;
};
console.log(multiply(4, 5)); // 20
|
函式表達式沒有提升,變數在賦值前是 undefined,呼叫會報錯:
1
2
3
4
5
| console.log(multiply(4, 5)); // TypeError: multiply is not a function
const multiply = function(a, b) {
return a * b;
};
|
箭頭函式(Arrow Function)#
ES6 引入的簡潔語法,是現代 JS 的主流寫法。根據情況可逐步省略語法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 完整寫法
const add = (a, b) => {
return a + b;
};
// 單行時,可省略大括號與 return(隱式回傳)
const add = (a, b) => a + b;
// 只有一個參數時,可省略括號
const double = n => n * 2;
// 沒有參數時,括號不可省略
const getRandom = () => Math.random();
// 回傳物件字面值時,需用括號包住,避免與函式大括號混淆
const toObj = n => ({ value: n });
|
預設參數(Default Parameters)#
呼叫時若未傳入引數(或傳入 undefined),則使用預設值:
1
2
3
4
5
6
7
| function greet(name = "訪客", greeting = "歡迎") {
return `${greeting},${name}!`;
}
console.log(greet("Alice", "你好")); // 你好,Alice!
console.log(greet("Alice")); // 歡迎,Alice!
console.log(greet()); // 歡迎,訪客!
|
其餘參數(Rest Parameters)#
用 ... 將剩餘的引數收集成陣列,必須放在最後一個參數:
1
2
3
4
5
6
7
8
9
10
11
12
13
| function sum(...nums) {
return nums.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
// 混合使用:固定參數 + 其餘參數
function log(level, ...messages) {
console.log(`[${level}]`, messages.join(" "));
}
log("INFO", "伺服器", "已啟動"); // [INFO] 伺服器 已啟動
|
展開運算子(Spread Operator)#
... 在呼叫函式或建立陣列/物件時,可將可迭代的值展開:
1
2
3
4
5
6
7
8
9
10
11
12
| // 展開陣列作為函式引數
const nums = [3, 1, 4, 1, 5];
console.log(Math.max(...nums)); // 5(等同於 Math.max(3, 1, 4, 1, 5))
// 合併陣列
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
// 複製並修改物件(後面的屬性覆蓋前面的)
const defaults = { color: "blue", size: "M" };
const custom = { ...defaults, color: "red" }; // { color: "red", size: "M" }
|
其餘參數(Rest)與展開運算子(Spread)都用 ...,但方向相反:
- Rest:把多個值收集成陣列(出現在函式定義)
- Spread:把陣列展開成多個值(出現在函式呼叫或字面值)
高階函式(Higher-Order Function)#
與前三種「定義方式」不同,高階函式是一種使用模式——只要一個函式滿足以下任一條件,就稱為高階函式:
內建的高階函式如:map、filter、reduce。除了內建的,以下是自訂的範例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 函式作為引數:applyTwice 接收函式 fn,對 x 執行兩次
function applyTwice(fn, x) {
return fn(fn(x));
}
const double = n => n * 2;
console.log(applyTwice(double, 3)); // double(double(3)) = double(6) = 12
// 函式作為回傳值:makeMultiplier 回傳一個新函式
function makeMultiplier(factor) {
return n => n * factor;
}
const triple = makeMultiplier(3); // triple 是 n => n * 3
console.log(triple(5)); // 15
|
閉包(Closure)#
當一個函式回傳另一個函式時,內層函式會記住它被建立時所在的作用域,即使外層函式已執行結束,內層函式仍能存取外層的變數。這種機制稱為閉包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function makeCounter() {
let count = 0; // count 存在於 makeCounter 的作用域
return {
increment: () => ++count, // 這些函式都記住了 count
decrement: () => --count,
value: () => count
};
}
const counter = makeCounter();
counter.increment(); // count = 1
counter.increment(); // count = 2
counter.decrement(); // count = 1
console.log(counter.value()); // 1
|
每次呼叫 makeCounter() 都會產生獨立的 count,兩個 counter 之間互不影響:
1
2
3
4
5
6
7
8
9
| const counterA = makeCounter();
const counterB = makeCounter();
counterA.increment();
counterA.increment();
counterB.increment();
console.log(counterA.value()); // 2
console.log(counterB.value()); // 1(獨立的 count)
|
立即執行函式(IIFE)#
定義後立刻執行的函式,常用來建立獨立作用域,避免變數污染全域:
1
2
3
4
5
6
| (function() {
const secret = "只在這個作用域內存在";
console.log(secret); // 可以存取
})();
console.log(secret); // ReferenceError:外部無法存取
|
現代 JS 有了 let/const 的區塊作用域,IIFE 較少使用,但在舊版程式碼中很常見。
純函式(Pure Function)#
純函式有兩個條件:
- 相同輸入,永遠得到相同輸出
- 沒有副作用(不修改外部狀態、不做 I/O)
1
2
3
4
5
6
7
8
9
10
11
| // 純函式:只依賴輸入,不修改外部狀態
const add = (a, b) => a + b;
const double = arr => arr.map(n => n * 2); // 回傳新陣列,不修改原陣列
// 非純函式:依賴外部變數(相同輸入可能得到不同輸出)
let tax = 0.1;
const calcPrice = price => price * (1 + tax); // tax 改變會影響結果
// 非純函式:有副作用(修改了外部狀態)
let total = 0;
const addToTotal = n => { total += n; }; // 修改了外部的 total
|
純函式容易測試、容易理解,是函數式程式設計的核心概念。
函式的 this#
一般函式的 this 指向呼叫它的物件;箭頭函式沒有自己的 this,而是繼承定義時所在的外層 this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| const obj = {
name: "物件",
regularFn: function() {
console.log(this.name); // this 指向 obj
},
arrowFn: () => {
// 箭頭函式在物件字面值中定義,外層是全域作用域
console.log(this.name); // undefined(全域 this 沒有 name)
},
// 常見陷阱:setTimeout 中的 this
delayedRegular: function() {
setTimeout(function() {
console.log(this.name); // undefined(this 變成全域或 undefined)
}, 100);
},
delayedArrow: function() {
setTimeout(() => {
console.log(this.name); // "物件"(繼承外層 this,即 obj)
}, 100);
}
};
obj.regularFn(); // 物件
obj.arrowFn(); // undefined
obj.delayedArrow(); // 物件
|
原則:在物件方法中需要用 this 時,用一般函式;回呼函式想繼承外層 this 時,用箭頭函式。
Reference#