美思 現代 [JavaScript] 程式設計:宣告和使用函式 (Function)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在 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 有兩個參數,分別是 baseexp,前者是底數,後者是指數。這個版本的指數運算支援正或負整數的指數運算。至於實作的部分只是簡單的數字運算,讀者應可自行閱讀。

我們另外寫了個工具函式 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),反覆收斂到 n1 為止。

我們先前講過,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 不會出現在命名空間中。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。