函數 Function

自訂函數的意義在於「封裝重複使用的程式碼」,而由定義的參數提供程式碼所需的資料。
參考 MSDN 函式:https://msdn.microsoft.com/zh-tw/library/yh6c50h7(v=vs.94).aspx

1建立函數

每個函數小括號內提供參數值,每個值以一個變數或文字來表示,不需先定義再呼叫,順序可以自訂,因 JavaScript 會先找尋函數定義,然後才執行程式碼。嚴格來說,技術上函數定義的變數稱作「參數(Parameters)」,而在呼叫時傳遞函數的值稱作「引數(Arguments)」,引數和參數不一定要同名

命名規則


定義函數


function 函數名稱 ( [參數 1] , [參數 2] ...) { ... }
          function funcName(){                          //不帶任何參數,表示這個函數無需傳遞值
              ...
          }
          function funcName(p1){                        //帶一個參數
              ...
          }
          function funcName(p1, p2){                    //多個參數需用逗號隔開
              ...
          }
          function funcName({num: 1, name: 'Domo'}){    //帶一個物件參數,建議還是先建立陣列或物件,再至函數的呼叫

          }
        

傳值呼叫函數


函數名稱 ( [引數 1] , [引數 2] ...)
          funcName();
          funcName(a1, a2);
          funcName({num: 1, name: 'Domo'});
        

            function init(){
              document.write('run init function.');
            }
            init();


            function helloFunc(name, age){
              document.write('Hello! ' + name + ', your age is ' + age + '.');
            }
            helloFunc('Domo', 24);


            var myObj = new Object({num: 3, name: 'Domo'});
            displayObj(myObj);
            function displayObj(obj){
              for(var p in obj){
                  document.write(p + ': ' + obj[p] + '
'); } }

2驗證函數的參數

以下三個函數的參數特色:

1. 不檢查型別的函數


因為 JavaScript 是弱型別語言的緣故,所以函數裡的參數和變數一樣都不用宣告型別
          function add(x, y){             //編寫一個相加的函數
            x + y;
          }
          add(2, 2);                      //如果是數值,則執行算術加法
          add('Hello, ', 'World!');       //如果是字串,將進行連接動作
        
解決方案是要自己增加類型檢查的方法
          function add(x, y){
            if((typeof x == 'number') && (typeof y == 'number')){
              ...
            }
          }
        

          function add(x, y){
            if((typeof x == 'number') && (typeof y == 'number')){
              document.write('
typeof is number => ' + (x + y)); }else{ document.write('origin => ' + (x + y)); } } add(2, 2); add('Hello', 'Domo');

2. 不檢查參數個數的函數


大部分其他語言中,當呼叫函數時提供的參數數量不正確時,會造成錯誤,但在 JavaScript 裡不會造成錯誤,不過函數可能無法正常運作。
arguments 變數的方法在 JavaScript 內建方法中到處可見,像是 concat() 方法就可以取出任意數量的字串值進行連接。
            call('A', 'B', 'C');
            function call(v1, v2, v3){
              document.write(arguments.length);
              for(var i = 0, count = arguments.length; i < count; i ++){
                document.write(arguments[i] + ', ');
              }
            }
            call(true, false, 'D', 0);
            call('D','E');
            call();
          

3. 沒有預設值的參數


JavaScript 函數中無法設定參數預設值(副作用是參數全部都可有可無),如果呼叫函數時未傳入某個參數值,其值為 undefined。
可用 typeof 運算子賦予預設值:
          function funcName(val){
            if(typeof val == 'undefined'){
              val = 'default value';        //指派預設值
            }else{
              ...
            }
          }
        

呼叫函數時無法跳過參數,假如要為第三個參數提供值而忽略第二個參數,則可使用 undefined 做為第二個參數值:
儘管如此,定義函數時將盡可能將參數的必要性從最高到最低依序排下來!
          function funcName(a, b, c){
              ...
          }
          funcName(true, undefined, false);
        

            function print(str, name, age){
              if(typeof name == 'undefined'){
                name = 'Nobody';
              }
              document.write('print => ' + str + '! ' + name + ', Your age is ' + age + '.');
            }

            print('Hello', 'Domo', 24);
            print('Hello', undefined, 24);
            print();
            
          

3參數值的傳遞方式

傳遞參數值有兩種方式:

1. 按值傳遞


簡單類型(數字、字串、布林值)都是按值傳遞,意味著實際上的變數並沒有傳給函數,而只是變數的值,因此在函數內改變簡單值,對函數外的變數沒有影響,兩個變數是不同記憶體。
            function change(x){
              document.write('In Function x => ' + x);
              x = 2;
              document.write('In Function change x => ' + x);
            }

            var a = 1;
            document.write('Before Function a => ' + a);
            change(a);
            document.write('After Function a => ' + a);             //原本的全域變數沒有被改變
          

2. 按址傳遞


複雜類型(陣列、物件)是按址傳遞,表示函數不接受複雜類型的值,而接收原變數的參照,函數內修改了這些變數,連帶影響函數外的變數內容,兩個變數指向同一塊記憶體。
按址傳遞特性的好處:複雜資料類型可作為容器,將複雜資料帶到函數以外。
            function change(x){
              document.write('In Function x.num => ' + x.num);
              x.num = 2;
              document.write('In Function change x.num => ' + x.num);
            }

            var a = {num: 1};
            document.write('Before Function a.num => ' + a.num);
            change(a);
            document.write('After Function a.num => ' + a.num);     //原本的全域變數被改變了
          

4從函數返回值

函數的另一個特徵是函數的返回值,提供函數與呼叫程式碼之間的交流,用 return 回傳。
即使 return 語句後還有程式碼,也會被直接終止函數執行,return 雖只會被執行一次,但沒有限制其內的 return 語句數量。
            var noReturnTag = noReturn(2);
            function noReturn(x){
                x ++;                             //沒有 return 或 return 不包含返回值,自動回傳 undefined
            }

            var checkTag = checkName('Domo');
            function checkName(x){                //可以列出多個 return
                if(typeof x == 'string') return true;
                else return false;
            }

            var numTag = addNum(2,2);
            function addNum(x, y){
                return x + y;                     //函數遇到第一個 return 就返回,其餘的程式碼一律不執行
                return x - y;
            }

            var arrTag = returnArr();
            function returnArr(){
                return [1, 2, 3];                 //回傳值為陣列
            }

            var objTag = returnObj();
            function returnObj(){
                return {name: 'Domo', num: 2};    //回傳值為物件
            }
          
參考 MSDN return 陳述式:https://msdn.microsoft.com/zh-tw/library/22a685h9(v=vs.94).aspx

5變數作用域

變數作用域是指變數存在和可用的範圍,全域(Global)就是一種作用域,在 JavaScript 檔案中,所有函數外部定義的變數都具備全域作用域,包括 Web 瀏覽器提供的特殊變數。
每個單獨的函數都有自己的區域作用域,定義函數時會建立一個新的作用域級別──區域作用域:
函數的參數 → 接收呼叫函數時所傳遞的值 → 自動具有函數級別(區域)作用域

全域變數永遠不要使用的原因(除了絕對必須的情況下):
  1. 有意或無意的和其他全域變數(例如 Web 瀏覽器或第三方程式庫提供的變數)產生衝突,此類衝突較難修復,稱作「命名空間汙染」或「命名空間混亂」。
  2. 全域變數容易被任何函數存取,任何函數都可能修改這些變數的值而導致臭蟲,稱作「存取控制缺失」(存取控制是對「誰」或者「什麼」能夠使用或修改某種資源的限制)。
  3. 易造成性能問題,在這種環境下始終需要追蹤全域變數,相較區域變數只在函數執行期間才需追蹤環境以及呈現他們所需的記憶體。
            var numPass = 2;
            var y = 0;
            function areaReturn(x){
              var z = x * 10;       //只要使用 var 關鍵字,在函數內宣告的變數就具有區域作用域,無法在函數外使用

              y = x;                //沒有加上 var 將存取全域變數

              var numPass = 0;      //如果區域變數與全域變數同名,以區域為優先,不存取全域變數的值

              return numPass + z;
            }
            areaReturn(numPass);
          
          
函數設計理論
如何定義自己的函數既是語法,也是設計問題,涉及到自訂函數的品質。一個適當的使用者自訂函數應盡可能被重複使用,意指如果網站只呼叫函數一次,就沒有必要被定義。針對函數,需有「黑盒子」的心態:程式人員不需要知道函數的內部結構,就能夠正確地使用函數,像是 JavaScript 內建函數即是如此。最後,使用全域函數要特別小心,因函數應該得到運作所需的資料,維持一條法則:函數越獨立,就越有用

6函數作為物件處理

JavaScript 的函數有非常獨特的特質:函數本身也是物件,確切地說是 Function 類型的物件,可以對 JavaScript 函數進行許多其他語言做不到的處理,以下是函數定義類型的兩種方式:

1. 作為變數值的函數


利用運算式建立函數,將其視為一個變數值,例如經常使用的 window.onload = init; 賦予變數的任何值也可以填入物件屬性,因後者就是和物件相關的變數。
變數的值 → 函數的定義 → 指派給物件裡的屬性使用
          var getTwo = function(){    //用運算式建立函數
            return 2;
          };
          getTwo();                   //這是一個 Function 物件,可以直接被呼叫

          window.onload = getTwo;     //也可填入物件屬性方式呼叫函數,不可加小括號;若加小括號變成把回傳值返回給屬性

          //可以跳過函數命名和建立函數變數的步驟,直接將函數運算式填給物件屬性,稱作「匿名函數」
          window.onload = function(){
            getTwo();
          };
        

2. 作為引數值的函數


將函數當作物件使用的方式傳遞函數定義給另一個函數,就像處理其他引數值一樣,只有在被呼叫的函數預期的引數是函數時才有意義。
傳遞函數定義 → 傳給另一個函數
          var someFunc = function(){
            ...
          };
          otherFunc(someFunc);          //傳給另一個函數
        
            var numOrder = ['One', 'four', 'three', 'Two'];
            numOrder.sort();                    //一般 sort() 方法僅能按照字母順序排列,並先大寫後小寫

            function compareNumber(x, y){
              x = x.toLowerCase();              //將元素都改成小寫比較
              y = y.toLowerCase();
              if(x > y)	return 1;               //如果 sort() 引數中提供函式,此函式必須傳回 1, -1, 0
              else if(x < y)	return -1;
              else	return 0;
            };

            numOrder.sort(compareNumber);       //告訴 sort() 方法以 compareNumber 函數代替預設機制
          
參考 MSDN sort 方法:https://msdn.microsoft.com/zh-tw/library/4b4fbfhk(v=vs.94).aspx

其他新陣列函數



下列為 ECMAScript5 增加的新陣列函數,需要使用者自訂函數才能運作:
            var arr1 = [10, 11, 12];
            var sum = 0;
            arr1.forEach(
              function addNumber(value) { sum += value; }   //對每個元素做 addNumber 動作
            );


            var arr2 = [1, true, 'test'];
            function mixFunc(value){
              return (typeof value == 'string');
            }
            if(arr2.every){                          //可以使用 every() -> 直接用 every() 確認每個元素是不是字串
              var result = arr2.every(mixFunc);      //每個元素都為字串才回傳 true;否則為 false
            }else{                                   //不能使用 every() -> 改用 for loop 確認每個元素是不是字串
              var result = true;
              for(var i = 0; i < arr2.length; i++){
                if(!mixFunc(arr2[i])){
                  result = false;
                  break;
                }
              }
            }


            var arr3 = [1, 15, 4, 10, 11, 22];
            function CheckIfEven(value, index, ar) {
              if (value % 2 == 0)
                return true;
            }
            var evens = arr3.some(CheckIfEven);             //只要有一個元素為偶數就回傳 true;否則為 false


            var arr4 = [10, 20, 30];
            function AreaOfCircle(radius) {
              var area = Math.PI * (radius * radius);
              return area.toFixed(0);
            }
            var areas = arr4.map(AreaOfCircle);             //將原陣列每個元素修改為計算的值,再回傳修改後的陣列


            var arr5 = [5, "element", 10, "the", true];
            var filter = arr5.filter(                       //將原陣列過濾掉非字串的元素,再回傳過濾後的陣列
              function (value) {
                return (typeof value === 'string');
              }
            );


            function addRounded (prevVal, currVal) {
              return prevVal + Math.round(currVal);
            }
            var arr6 = [10.9, 15.4, 0.5];
            var reduce = arr6.reduce(addRounded, 0);        //將所有元素四捨五入後相加,再回傳相加後的值
          
參考 MSDN forEach 方法:https://msdn.microsoft.com/zh-tw/library/ff679980(v=vs.94).aspx
參考 MSDN every 方法:https://msdn.microsoft.com/zh-tw/library/ff679981(v=vs.94).aspx
參考 MSDN some 方法:https://msdn.microsoft.com/zh-tw/library/ff679978(v=vs.94).aspx
參考 MSDN map 方法:https://msdn.microsoft.com/zh-tw/library/dn263029(v=vs.94).aspx
參考 MSDN filter 方法:https://msdn.microsoft.com/zh-tw/library/ff679973(v=vs.94).aspx
參考 MSDN reduce 方法:https://msdn.microsoft.com/zh-tw/library/ff679975(v=vs.94).aspx

7上下文和 this

以 JavaScript 檔案的每行程式碼來說,都有執行的上下文(Content,亦稱「執行上下文(Execution context)」)。函數主體內的程式也在不同的上下文進行操作,另一個函數主體的程式碼也有其上下文;執行函數結束時,就返回到呼叫函數時的上下文;每個上下文存在著不同的物件,不同的屬性也有著不一樣的值,而涉及上下文的關鍵工具是特殊物件 this。
當函數不與物件相關聯時,這個函數實際上就是全域物件(例如瀏覽器的 window)的一部份,表示 this 通常指的是全域物件。
            var n = 2;
            n.toFixed(2);      //在 toFixed() 方法中,this 引用的是變數 n,如此一來方法的內部結構就能存取該變數的值

            var employee = {
              firstName: 'Domo',
              lastName: 'Huang',
              getName: function(){
                //this 為物件提供參照自身屬性的方法,因函數是一個物件,所以可將其指派給其他物件的屬性,也包括自訂物件
                return this.firstName + ', ' + this.lastName;
              }
            };
            employee.getName(); //便可使用 obj.method() 執行屬性中定義的函數
          
參考 MSDN this 陳述式:https://msdn.microsoft.com/library/w062xezx(v=vs.94).aspx

8其他函數型態

匿名函數


匿名函數會在以下情況中使用:
  1. 賦值給變數
  2. 賦值給物件屬性
  3. 當成函數呼叫中的傳遞值
          var a = function(){               //賦值給變數
            ...
          };

          window.onload = function(){       //賦值給物件屬性
            ...
          };

          array.forEach(function(){         //當成函數呼叫中的傳遞值
            ...
          });
        

另一種匿名函數的用法是「立即呼叫函數(Immediately invoked function)」,先建立一個匿名函數,將其包裝在函數呼叫中:
好處是可以讓自己的變數和其他程式碼脫離全域作用域。
          (function(){         //將函數封裝在小括號中,在整個結構後面加上呼叫函數所需的小括號
            ...
          })();
        
            var a = function(){                           //賦值給變數 a
              return 'run func1';
            };document.write('func1 => ' + a());          //呼叫 a 函數


            var obj = new Object();
            obj.name = function(){                        //賦值給物件 obj 的 name 屬性
              var b = 'run func2';
              document.write('func2 => ' + b);
            };
            obj.name();                                   //呼叫 obj 的 name 方法


            function callFunc(x){
              return 'run func3 => '+ x;
            }
            var c = callFunc(function(){ return 2; }());  //匿名函數回傳的值作 callFunc 函數呼叫的傳遞值,再賦值給 c
            document.write('func3 => ' + c);


            (function(){
              document.write('run func4');                //立即呼叫函數
            })();
          

巢狀函數


函數內建立另一個函數,因為物件擁有方法,函數就是一種物件,實際上就是定義外部函數的一種方法,巢狀函數有自己的作用域,內部函數自動有父函數區域變數的存取權。
            function outerFunc(outerArg){
              var a = 'I\'m inside.';
              function innerFunc(innerArg){
                document.write('x => ' + outerArg);
                document.write('y => ' + a);
                document.write('z => ' + innerArg);
              }
              innerFunc(true);
            }
            outerFunc('I\'m outside.');
          
內部函數從全域作用域來看是「隱藏」的,意味著無法從外部函數(主函數)以外呼叫內部函數,如果呼叫外部函數,那麼內部函數也不會是全域作用域的一部份。

遞迴函數


遞迴的概念在理論上相當簡單,實際應用時卻很複雜,遞迴是函數呼叫自身的動作,可能出現在任何允許自訂函數的程式語言中,最容易的遞迴方法之一是「階乘函數」。
遞迴是用在重複未知次數時的解決方案(例如瀏覽樹型資料結構),或是任務應用程式有子任務,且任務又有一到多個子任務。
瀏覽器執行遞迴的次數有一定的限制,因遞迴作業取決於大量記憶體(原始函數內的 return 語句在所有遞迴呼叫返回值之前無法執行),在某些情況下,簡單迴圈也能完成相同目標,且不需較大的記憶體,但執行速度相較於遞迴慢
            function factorial(n){
              if(n <= 1)
                return 1;
              else
                return n * factorial(n - 1);          //使用遞迴函數計算階乘
            }
            document.write('5! => ' + factorial(5));


            function forloop(n){
              for(var product = 1; n > 1; n--){       //使用 for 迴圈計算階乘
                product *= n;
              }
              return product;
            }
          
參考 MSDN 遞迴:https://msdn.microsoft.com/zh-tw/library/wwbyhkx4(v=vs.94).aspx
DEMO MEMO