那些年,我們應該瞭解的 JAVASCRIPT 小知識 - 函式 function

JavaScript 是需要被瞭解

前一篇,我們針對了物件, 數字, 字串, 陣列, 迴圈提供了一些小知識,續前篇,我們再談一些關於函式的小知識。

函式小知識

函式(function)可說是 JavaScript 裡頑皮又最有趣的…(函式)物件。還記的「萬物皆物件,萬物皆可放」這兩句話嗎。

具名函式

function add(x, y) {
    var total = x + y;
    return x + y;
}

這是一個非常完整的函式,正式名稱叫具名函式,但一般我們簡稱的函式就是具名函式:

  1. 名稱,name
  2. 參數,x 與 y
  3. 區域變數,total
  4. 運算,x + y
  5. 回傳值,return

目前為止都是中規中舉,函式是拿來呼叫的,所以我們來不正常呼叫吧!

// 傳入兩個 undefined
// undefined + undefined = NaN (就不是整數呀)
document.writeln(add());              // NaN 
document.writeln(add(5, 9, 999));     //  14,函式忽略第三個參數

第二個才是好玩的地方,JavaScript 對於多傳入的參數給省略了,但其實不是,因為我們可以在函式之內存取一個 arguments 的變數,arguments 內有傳遞給函式的所有值。這與一般程式語言的用法不同,一般程式語言的函式,你定義幾個參數就是能傳入幾個參數,頂多是依參數的不同設計為重載,但你能傳遞的參數個數還是被函式所限制,JavaScript 透過 arguments 變數的幫忙,可以把所有函式設計成接受無限的傳入值。讓我們改寫原始函式:

function avg() {
    var sum = 0;
    // 透過 arguments.length 取得要加總的次數
    for (var i = 0, total = arguments.length; i < total; i++) {
        sum += arguments[i];
    }
    return sum / total; // 平均值
}

document.writeln(avg(1,3,5,7,9,2,4,6,8,10));    // 5.5

用起來好像很不錯,但 arguments 變數有一個問題,就是它只接受用 ,(逗點)分隔的參數值,所以如果我們還想要處理來自陣列的值,我們就要再寫一個函式來處理陣列:

// arr 為陣列
function avgArray(arr) {
    var sum = 0;
    // 透過 arr.length 取的要加總的次數
    for (var i = 0, total = arr.length; i < total; i++) {
        sum += arr[i];
    }
    return sum / total;
}

var arr = [2,4,6,8,10,1,3,5,7,9];
document.writeln(avgArray(arr));    // 5.5

明明是一樣的事,為什麼要做兩分工呢?還好,函式也是物件,要讓 arguments 變數也可以接受陣列值,最要一點小技巧,我們必須使用到一個函式物件所提供的方法 apply()

// Function.apply(thisArg, arrayArg);
// thisArg, 物件內部的 this 會等於 thisArg;
// arrayArg,傳入一個陣列的參數
var arr = [2,4,6,8,10,1,3,5,7,9];
document.writeln(avg.apply(null, arr)); // 5.5

這邊我們先關注在 ArrayArg 這個參數就好,這行 avg.apply(null, arr); 翻譯成白話就是「執行 avg 函式物件,而且幫傳入二個參數,一個給 this 使用,另一個是陣列的參數」,經過如此改良之後,arguments 變數就看得懂傳入陣列是參數。或許這樣想,經過 Function.apply() 的幫忙,它把我們要傳入的陣列 [2,4,6,8,10,1,3,5,7,9] 變成 2,4,6,8,10,1,3,5,7,9 (去除陣列實字符號[])然後傳入。

還有一個函式物件方法為 call(),用法上與 apply() 相似,差異只有第二個陣列參數,我們必須一個一個傳入,例如:avg.call(null, 1,3,5,7,9,2,4,6,8,10)

匿名函式

匿名函式,人如其名,也就是沒有名稱的函式,它是函式中非常強大的武器,有點可以為所欲為,要小心服用。我們來看例子:

var avg = function() {
    var sum = 0;
    // 透過 arguments.length 取得要加總的次數
    for (var i = 0, total = arguments.length; i < total; i++) {
        sum += arguments[i];
    }
    return sum / total; // 平均值
}
document.writeln(avg.apply(null, arr)); // 5.5

把原先的 function avg() {} 的名稱改使用為 var avg = function() {},此語法就運作的意義上是相同,但此技巧帶來的效果非常強大,我自可以在任何原本放表達式的地方換成放入一個完整的函式定義。

var a = 1;
var b = 2;
// 1. 最外( )包匿名函式
(function() {
    var b = 3;  // 此 b 是匿名函式的區域變數
    a += b;     // 這裡改變了全域變數 a
})();   // 2. 這裡的 () 呼叫自己執行一次

document.writeln(a);    // 4
document.writeln(b);    // 2

匿名函式裡的 b 被保護起來了。

變數的範圍

這裡先我們跳離開函式,來討論一個在 JavaScript 裡極為重要的內容。在 JavaScript 裡變數是沒有範圍(scope,或稱影響範圍,作用範圍)的觀念,範圍也就是常說的【全域變數】與【區域變數】,範圍控制變數與參數的可見性與生命週期。在 JavaScript 裡任何地方宣告的變數的範圍都是【全域變數】,好玩吧!但在上面的小小範例中,我們說明了一個極為重要的內容,在函式內宣告的變數為【區域變數】,這是 JavaScript 裡唯一範圍是非全域,會產生區域變數的方法。(另一種 Closure 也是函式的變型,簡言之,函式是唯一產生區域變數的方法)。我們看個簡單的例子,

// 計算平方
function square(num) {
    total = num * num;
    return total;
}

var total = 50;
var number = square(20);
console.log(total);         // 這裡的 total 是多少

number = 20 * 20 = 400,這裡沒問題,重點在最後一行的 total 會是多少?答案也是 400,為什麼呢?前面不是剛說「在函式內宣告的變數為【區域變數】」,我沒教錯。JavaScript 給我們帶來很多方便性,它是一個很隨性的語言,變數使用前要宣告不宣告都可以,問題在於未宣告的變數都以【全域變數】來處理,也就造成函式內部的 total 是以全域變數運作,它覆寫了原始 var total 這個宣告的值。我們整理一下:

  1. 非 function 宣告(var)都是作用於 global
  2. function 內宣告(var)都是作用於 local
  3. 小心 function 內的 global 會直接影響外部 global

正確的算式應該為:

function square(num) {
    var total = num * num;
    return total;
}

減少全域變數

減少全域變數的使用,可以確保 JavaScript 的程式品質,和提升除錯的容易度。減少全域變數的其中一種方式,幫你的 JavaScript 應用程式建立唯一一個全域變數的物件集合。

// 透過一個物件來整理整個應用程式的全域變數
var BruceApps = {};

// 個人資料相關全域變數
BruceApps.person = {
    "first-name": "Bruce",
    "last-name": "Chen",
    "nickname" : "KKBruce",
    "age": 18,
    "live": "Taiwan",
    "work": "Developer"
};

// 個人社交相關全域變數
BruceApps.social = {
    "www": "http:\/\/www.kkbruce.net",
    "blog": "http:\/\/blog.kkbruce.net",
    "plurk": "http:\/\/www.plurk.com\/kk_bruce\/invite",
    "Google+專頁": "http:\/\/gplus.to/kkbruce",
    "twitter": "http:\/\/twitter.com\/kkbruce",
    "weibo": "http:\/\/weibo.com\/kkbruce"
};

// 要取用時
console.log(BruceApps.social.GooglePlus);

還有一種方式是Closure,這裡直接介紹太危險,先把函式摸熟,這樣瞭解Closure時,就很簡單些了。

遞迴呼叫

回到函式身上,JavaScript 的函式是可以遞迴呼叫的,這在處理 DOM 元素時特別有用,但問題來了,我是匿名函式你怎麼呼叫呢?匿名呼叫匿名,這樣嗎?arguments 物件除了提供取得一系列參數外,還提供一個 callee 屬性,此屬性能讓我們指向目前的函式。例如:

function selfFunCounter() {
    // arguments.callee 指向目前函式
    // arguments.callee.count 確認是否有此屬性
    if (!arguments.callee.count) {
        arguments.callee.count = 0; // 沒有,增加設定屬性及值為 0
    }
    return arguments.callee.count += 1; // 每次 +1
};
console.log(selfFunCounter());  // 1
console.log(selfFunCounter());  // 2
console.log(selfFunCounter());  // 3

沒有留言:

張貼留言

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。