前言
在 JavaScript 中,函式是相當重要的概念。至少有下列數種和函式相關的主題:
- 包住可重覆使用的程式碼區塊
- 控制變數的生命週期 (scope)
- 撰寫基於物件的程式 (object-based programming)
- 撰寫函數式程式 (functional programming)
在單篇文章中,無法涵蓋全部的內容,我們會拆成數篇文章來講。本文僅介紹第一部分。
宣告和使用函式
撰寫 JavaScript 函式的虛擬碼如下:
function fn (params) {
/* Implement function here. */
}
由此可知,寫 JavaScript 函式需要以下要件:
- 使用
function
保留字宣告函式 fn
是函式的識別字params
是函式的參數,可零到多個- 函式的實作部分用一對大括號
{
}
包起來 - 用
return
保留字回傳值 (沒顯示在虛擬碼中)
由於 JavaScript 的值可以當資料使用,所以可以寫成以下的形式:
let fn = function (params) {
/* Implement function here. */
};
這時候的程式碼是敘述而非宣告,所以尾端要加分號 ;
。
除了換個寫法外,更重要的觀念是 JavaScript 的函式是資料,所以可支援函數式程式設計。
我們來看一個實際的例子。在這個例子中,我們寫了一個指數運算函式 pow
:
function pow (base, exp) {
if (exp === 0) {
return 1;
}
let result = 1;
if (exp > 0) {
result *= base;
for (let i = 1; i < exp; i++) {
result *= base;
}
} else if (exp < 0) {
result /= base;
for (let i = -1; i > exp; i--) {
result /= base;
}
}
return result;
}
// Home-made `assert`
function assert(cond, msg) {
if (!(cond)) {
throw (msg ? msg : 'Assertion failed');
}
}
assert(pow(3, 0) === 1, "It should be 1");
assert(pow(3, 2) === 3 * 3, "It should be 9");
assert(Math.abs(pow(3, -2) - 1 / 9) < 0.00001, "It should be 1 / 9");
函式 pow
有兩個參數,分別是 base
和 exp
,前者是底數,後者是指數。這個版本的指數運算支援正或負整數的指數運算。至於實作的部分只是簡單的數字運算,讀者應可自行閱讀。
我們另外寫了個工具函式 assert
,這是因為 JavaScript 沒有內建的斷言 (assertion)。這個函式在我們的文章中出現多次,讀者應該已經相當熟悉。
回傳多個值
JavaScript 無法同時回傳多個值,替代的方法是用容器包住多個值後回傳。常見的內建容器是陣列或物件實字。以下範例回傳陣列:
function divmod (a, b) {
let div = Math.floor(a / b);
let mod = Math.floor(a % b);
return [div, mod];
}
/* Home-made `assert` */
function assert(cond, msg) {
if (!(cond)) {
throw (msg ? msg : 'Assertion failed');
}
}
/* Destructing assignment. */
let [div, mod] = divmod(9, 2);
assert(div === 4, "div should be 4");
assert(mod === 1, "mod should be 1");
現在的 JavaScript 支援解構運算,對於回傳多個值的情境可用更自然的語法取出值。
若要回傳陣列,建議不要超過 3 個值,因為記憶過長的回傳值對函式使用者會造成記憶上的負擔。
我們將上述範例改寫,回傳物件實字:
function divmod (a, b) {
let div = Math.floor(a / b);
let mod = Math.floor(a % b);
return {
div: div,
mod: mod
};
}
// Destructing assignment.
let {div, mod} = divmod(9, 2);
assert(div === 4, "div should be 4");
assert(mod === 1, "mod should be 1");
// Home-made `assert`
function assert(cond, msg) {
if (!(cond)) {
throw msg;
}
}
回傳物件實字的好處在於不用記憶回傳值位置,對於回傳值數量稍微能容忍一點。但回傳值仍不應該太複雜。若發現回傳值很複雜,應該考慮將回傳值包在物件後再回傳。
傳值和傳址
對於基礎型別來說,JavaScript 函式在傳遞參數時,會將參數拷貝一份。所以,以下例子的 a
在函式內改變了,卻不影響函式外:
// Home-made `assert`
function assert (cond, msg) {
if (!(cond)) {
throw (msg ? msg : 'Assertion failed');
}
}
function addOne (a) {
let temp = a;
a++;
assert(a === temp + 1, "a should be one more");
}
let n = 3;
addOne(n);
assert(n === 3, "n should be 3");
但傳遞物件實字時,則是傳遞物件實字的位址,故會影響到物件實字的屬性。參考下例:
/* Home-made `assert` */
function assert (cond, msg) {
if (!(cond)) {
throw msg;
}
}
function addOne (obj) {
let temp = obj.item;
obj.item++;
assert(obj.item === temp + 1, "a should be one more");
}
let obj = {};
obj.item = 3;
addOne(obj);
assert(obj.item === 4, "obj.item should be 4");
一般來說,我們不應該在函式中修改參數。若需要和資料連動的函式,應該將其包成物件。我們會在後文介紹 JavaScript 的物件導向程式。
遞迴函式
在電腦程式中,遞迴就是把問題實體拆成更小的問題實體,在呼叫函式的過程中逐漸收斂問題實體,以取得解答。JavaScript 函式也支援遞迴呼叫。
一個常見的遞迴函式實例是階乘 (factorial)。參考以下範例:
function fac (n) {
assert(n >= 0, "n should be larger than or equal to zero");
if (n === 0 || n === 1) {
return 1;
}
return n * fac(n - 1);
}
// Home-made `assert`
function assert(cond, msg) {
if (!(cond)) {
throw msg;
}
}
assert(fac(5) === 120, "fac(5) should be 120");
在這個例子中,我們計算 fac(n)
的方式,是將 n
乘上更小的實體 fac(n-1)
,反覆收斂到 n
為 1
為止。
我們先前講過,JavaScript 的函式視為資料。所以我們可將上例改寫如下:
let fac = function f (n) {
assert(n >= 0, "n should be larger than or equal to zero");
if (n === 0 || n === 1) {
return 1;
}
return n * f(n - 1);
};
// Home-made `assert`
function assert (cond, msg) {
if (!(cond)) {
throw msg;
}
}
assert(fac(5) === 120, "fac(5) should be 120");
這時候,我們給函式物件額外的識別字 f
,便於遞迴呼叫。經筆者實測,f
不會出現在命名空間中。