函式(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)#

與前三種「定義方式」不同,高階函式是一種使用模式——只要一個函式滿足以下任一條件,就稱為高階函式:

  • 接收另一個函式作為參數
  • 回傳一個函式

內建的高階函式如:mapfilterreduce。除了內建的,以下是自訂的範例:

 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)#

純函式有兩個條件:

  1. 相同輸入,永遠得到相同輸出
  2. 沒有副作用(不修改外部狀態、不做 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#