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

JavaScript 是需要被瞭解

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

函式小知識

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

具名函式

1function add(x, y) {
2    var total = x + y;
3    return x + y;
4}

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

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

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

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

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

01function avg() {
02    var sum = 0;
03    // 透過 arguments.length 取得要加總的次數
04    for (var i = 0, total = arguments.length; i < total; i++) {
05        sum += arguments[i];
06    }
07    return sum / total; // 平均值
08}
09 
10document.writeln(avg(1,3,5,7,9,2,4,6,8,10));    // 5.5

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

01// arr 為陣列
02function avgArray(arr) {
03    var sum = 0;
04    // 透過 arr.length 取的要加總的次數
05    for (var i = 0, total = arr.length; i < total; i++) {
06        sum += arr[i];
07    }
08    return sum / total;
09}
10 
11var arr = [2,4,6,8,10,1,3,5,7,9];
12document.writeln(avgArray(arr));    // 5.5

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

1// Function.apply(thisArg, arrayArg);
2// thisArg, 物件內部的 this 會等於 thisArg;
3// arrayArg,傳入一個陣列的參數
4var arr = [2,4,6,8,10,1,3,5,7,9];
5document.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)

匿名函式

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

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

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

01var a = 1;
02var b = 2;
03// 1. 最外( )包匿名函式
04(function() {
05    var b = 3;  // 此 b 是匿名函式的區域變數
06    a += b;     // 這裡改變了全域變數 a
07})();   // 2. 這裡的 () 呼叫自己執行一次
08 
09document.writeln(a);    // 4
10document.writeln(b);    // 2

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

變數的範圍

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

1// 計算平方
2function square(num) {
3    total = num * num;
4    return total;
5}
6 
7var total = 50;
8var number = square(20);
9console.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

正確的算式應該為:

1function square(num) {
2    var total = num * num;
3    return total;
4}

減少全域變數

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

01// 透過一個物件來整理整個應用程式的全域變數
02var BruceApps = {};
03 
04// 個人資料相關全域變數
05BruceApps.person = {
06    "first-name": "Bruce",
07    "last-name": "Chen",
08    "nickname" : "KKBruce",
09    "age": 18,
10    "live": "Taiwan",
11    "work": "Developer"
12};
13 
14// 個人社交相關全域變數
15BruceApps.social = {
16    "www": "http:\/\/www.kkbruce.net",
17    "blog": "http:\/\/blog.kkbruce.net",
18    "plurk": "http:\/\/www.plurk.com\/kk_bruce\/invite",
19    "Google+專頁": "http:\/\/gplus.to/kkbruce",
20    "twitter": "http:\/\/twitter.com\/kkbruce",
21    "weibo": "http:\/\/weibo.com\/kkbruce"
22};
23 
24// 要取用時
25console.log(BruceApps.social.GooglePlus);

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

遞迴呼叫

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

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

沒有留言:

張貼留言

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