1. 程式人生 > >函式的實參和形參、作為值的函式

函式的實參和形參、作為值的函式

函式的實參和形參

JavaScript中的函式定義並未指定函式形參的型別,函式呼叫也未對傳入的實參值做任何型別檢查。實際上,JavaScript函式呼叫甚至不檢查傳入形參的個數。

  • 可選形參

當呼叫函式的時候傳入的實參比函式宣告時指定的形參個數要少,剩下的形參都將設定為undefined值。因此在呼叫函式時形參是否可選以及是否可以省略應當保持較好的適應性。為了做到這一點,應當給省略的引數賦一個合理的預設值,來看這個例子:

//將物件o中可列舉的屬性名追加至陣列a中,並返回這個陣列a
//如果省略a,則建立一個新陣列並返回這個新陣列
function getPropertyNames(o,/*optional*/a){
    if(a===undefined)a=[];//如果未定義,則使用新陣列
    for(var property in o)a.push(property);
    return a;
}

//這個函式呼叫可以傳入1個或2個實參
var a=getPropertyNames(o);//將o的屬性儲存到一個新陣列中
getPropertyNames(p,a);//將p的屬性追加至陣列a中

如果在第一行程式碼中不使用if語句,可以使用“||”運算子,這是一種習慣用法:

a=a||[];

回憶一下,“||”運算子,如果第一個實參是真值的話就返回第一個實參;否則返回第二個實參。在這個場景下,如果作為第二個實參傳入任意物件,那麼函式就會使用這個物件。如果省略掉第二個實參(或者傳遞null以及其他任何假值),那麼就新建立一個空陣列,並賦值給a。

需要注意的是,當用這種可選實參來實現函式時,需要將可選實參放在實參列表的最後。那些呼叫你的函式的程式設計師是沒辦法省略第一個實參並傳入第二個實參的,它必須將undefined作為第一個實參顯式傳入。同樣注意在函式定義中使用註釋/*optional*/來強調形參是可選的。

  • 可變長的實參列表:實參物件

當呼叫函式的時候傳入的實參個數超過函式定義時的形參個數時,沒有辦法直接獲得未命名值的引用。引數物件解決了這個問題。在函式體內,識別符號arguments是指向實參物件的引用,實參物件是一個類陣列物件,這樣可以通過數字下標就能訪問傳入函式的實參值,而不用非要通過名字來得到實參。

假設定義了函式f,它的實參只有一個x。如果呼叫這個函式時傳入兩個實參,第一個實參可以通過引數名x來獲得,也可以通過arguments[0]來得到。第二個實參只能通過arguments[1]來得到。此外,和真正的陣列一樣,arguments也包含一個length屬性,用以標識其所包含元素的個數。因此,如果呼叫函式f()時傳入兩個引數,arguments.length的值就是2。

實參物件在很多地方都非常有用,下面的例子展示了使用它來驗證實參的個數,從而呼叫正確的邏輯,因為JavaScript本身不會這麼做:

function f(x,y,z)
{
//首先,驗證傳入實參的個數是否正確
    if(arguments.length!=3){
        throw new Error("function f called with"+arguments.length+
            "arguments,but it expects 3 arguments.");
    }
//再執行函式的其他邏輯...
}

需要注意的是,通常不必像這樣檢查實參個數。大多數情況下JavaScript的預設行為是可以滿足需要的:省略的實參都將是undefined,多出的引數會自動省略。

實參物件有一個重要的用處,就是讓函式可以操作任意數量的實參。下面的函式就可以接收任意數量的實參,並返回傳入實參的最大值(內建函式Max.max()的功能與之類似):

function max(/*...*/){
    var max=Number.NEGATIVE_INFINITY;//遍歷實參,查詢並記住最大值
    for(var i=0;i<arguments.length;i++)
    if(arguments[i]>max)max=arguments[i];//返回最大值
    return max;
}
var largest=max(1,10,100,2,3,1000,4,5,10000,6);//=>10000

類似這種函式可以接收任意個數的實參,這種函式也稱為“不定實參函式”(varargs function)。

注意,不定實參函式的實參個數不能為零,arguments[]物件最適合的應用場景是在這樣一類函式中,這類函式包含固定個數的命名和必需引數,以及隨後個數不定的可選實參。

記住,arguments並不是真正的陣列,它是一個實參物件。每個實參物件都包含以數字為索引的一組元素以及length屬性,但它畢竟不是真正的陣列。可以這樣理解,它是一個物件,只是碰巧具有以數字為索引的屬性。

陣列物件包含一個非同尋常的特性。在非嚴格模式下,當一個函式包含若干形參,實參物件的陣列元素是函式形參所對應實參的別名,實參物件中以數字索引,並且形參名稱可以認為是相同變數的不同命名。通過實參名字來修改實參值的話,通過arguments[]陣列也可以獲取到更改後的值,下面這個例子清楚地說明了這一點:

function f(x){
    console.log(x);//輸出實參的初始值
    arguments[0]=null;//修改實引數組的元素同樣會修改x的值
    console.log(x);//輸出"null"
}

如果實參物件是一個普遍陣列的話,第二條console.log(x)語句的結果絕對不會是null,在這個例子中,arguments[0]和x指代同一個值,修改其中一個的值會影響到另一個。

在ECMAScript 5中移除了實參物件的這個特殊特性。在嚴格模式下還有一點(和非嚴格模式下相比的)不同,在非嚴格模式中,函式裡的arguments僅僅是一個識別符號,在嚴格模式中,它變成了一個保留字。嚴格模式中的函式無法使用arguments作為形參名或區域性變數名,也不能給arguments賦值。

除了陣列元素,實參物件還定義了callee和caller屬性。在ECMAScript 5嚴格模式中,對這兩個屬性的讀寫操作都會產生一個型別錯誤。而在非嚴格模式下,ECMAScript標準規範規定callee屬性指代當前正在執行的函式。caller是非標準的,但大多數瀏覽器都實現了這個屬性,它指代呼叫當前正在執行的函式的函式。通過caller屬性可以訪問呼叫棧。callee屬性在某些時候會非常有用,比如在匿名函式中通過callee來遞迴地呼叫自身。

function f(x){
    console.log(x);//輸出實參的初始值
    arguments[0]=null;//修改實引數組的元素同樣會修改x的值
    console.log(x);//輸出"null"
}

 

  • 將物件屬性用做實參

當一個函式包含超過三個形參時,對於程式設計師來說,要記住呼叫函式中實參的正確順序實在讓人頭疼。每次呼叫這個函式時都要不厭其煩地查閱文件,為了不讓程式設計師每次都翻閱手冊這麼麻煩,最好通過名/值對的形式來傳入引數,這樣引數的順序就無關緊要了。為了實現這種風格的方法呼叫,定義函式的時候,傳入的實參都寫入一個單獨的物件之中,在呼叫的時候傳入一個物件,物件中的名/值對是真正需要的實引數據。下面的程式碼就展示了這種風格的函式呼叫,這種寫法允許在函式中設定省略引數的預設值:

//將原始陣列的length元素複製至目標陣列
//開始複製原始陣列的from_start元素
//並且將其複製至目標陣列的to_start中
//要記住實參的順序並不容易

function arraycopy(/*array*/from,/*index*/from_start,/*array*/to,/*index*/to_start,
                   /*integer*/length)
{
//邏輯程式碼
}
//這個版本的實現效率稍微有些低,但你不必再去記住實參的順序
//並且from_start和to_start都預設為0
function easycopy(args){
    arraycopy(args.from,
        args.from_start||0,//注意這裡設定了預設值
        args.to,
        args.to_start||0,args.length);
}

//來看如何呼叫easycopy()
var a=[1,2,3,4],b=[];
easycopy({from:a,to:b,length:4});
  • 實參型別

JavaScript方法的形參並未宣告型別,在形參傳入函式體之前也未做任何型別檢查。可以採用語義化的單詞來給函式實參命名,或者像剛才的示例程式碼中的arraycopy()方法一樣給實參補充註釋,以此使程式碼自文件化,對於可選的實參來說,可以在註釋中補充一下“這個實參是可選的”。當一個方法可以接收任意數量的實參時,可以使用省略號:

function max(/*number...*/){/*程式碼區*/}
JavaScript在必要的時候會進行型別轉換。因此如果函式期望接收一個字串實參,而呼叫函式時傳入其他型別的值,所傳入的值會在函式體內將其用做字串的地方轉換為字串型別。所有的原始型別都可以轉換為字串,所有的物件都包含toString()方法(儘管不一定有用),所以這種場景下是不會有任何錯誤的。

然而事情不總是這樣,回頭看一下剛才提到的arraycopy()方法。這個方法期望它的第一個實參是一個數組。當傳入一個非陣列的值作為第一個實參時(通常會傳入類陣列物件),儘管看起來是沒問題的,實際上會出錯。除非所寫的函式是隻用到一兩次的“用完即丟”函式,你應當新增類似的實參型別檢查邏輯,因為寧願程式在傳入非法值時報錯,也不願非法值導致程式在執行時報錯,相比而言,邏輯執行時的報錯訊息不清晰且更難處理。下面這個例子中的函式就做了這種型別檢查。注意這裡使用了isArrayLike()函式:

//返回陣列(或類陣列物件)a的元素的累加和
//陣列a中必須為數字、null和undefined的元素都將忽略
function sum(a){
    if(isArrayLike(a)){
        var total=0;
        for(var i=0;i<a.length;i++){//遍歷所有元素
            var element=a[i];
            if(element==null)continue;//跳過null和undefined
            if(isFinite(element))total+=element;
            else throw new Error("sum():elements must be finite numbers");
        }
        return total;
    }
    else throw new Error("sum():argument must be array-like");
}

這裡的sum()方法進行了非常嚴格的實參檢查,當傳入非法的值時會給出容易看懂的錯誤提示資訊。但當涉及類陣列物件和真正的陣列(不考慮陣列元素是否是null還是undefined),這種做法帶來的靈活性其實並不大。

JavaScript是一種非常靈活的弱型別語言,有時適合編寫實參型別和實參個數的不確定性的函式。接下來的flexisum()方法就是這樣(可能走向了一個極端)。比如,它可以接收任意數量的實參,並可以遞迴地處理實參是陣列的情況,這樣的話,它就可以用做不定實參函式或者實參是陣列的函式。此外,這個方法儘可能的在丟擲異常之前將非數字轉換為數字:

function flexisum(a){
    var total=0;
    for(var i=0;i<arguments.length;i++){
        var element=arguments[i],n;
        if(element==null)continue;//忽略null和undefined實參
        if(isArray(element))//如果實參是陣列
            n=flexisum.apply(this,element);//遞迴地計算累加和
        else if(typeof element==="function")//否則,如果是函式...
            n=Number(element());//呼叫它並做型別轉換
        else
            n=Number(element);//否則直接做型別轉換
        if(isNaN(n))//如果無法轉換為數字,則丟擲異常
            throw Error("flexisum():can't convert"+element+"to number");
        total+=n;//否則,將n累加至total
    }
    return total;
}

作為值的函式

函式可以定義,也可以呼叫,這是函式最重要的特性。函式定義和呼叫是JavaScript的詞法特性,對於其他大多數程式語言來說亦是如此。然而在JavaScript中,函式不僅是一種語法,也是值,也就是說,可以將函式賦值給變數,儲存在物件的屬性或陣列的元素中,作為引數傳入另外一個函式等。

為了便於理解JavaScript中的函式是如何用做資料的以及JavaScript語法,來看一下這樣一個函式定義:

function square(x){return x*x;}
這個定義建立一個新的函式物件,並將其賦值給變數square。函式的名字實際上是看不見的,它(square)僅僅是變數的名字,這個變數指代函式物件。函式還可以賦值給其他的變數,並且仍可以正常工作:

var s=square;//現在s和square指代同一個函式
square(4);//=>16
s(4);//=>16

 除了可以將函式賦值給變數,同樣可以將函式賦值給物件的屬性。當函式作為物件的屬性呼叫時,函式就稱為方法:

var o={square:function(x){return x*x;}};//物件直接量
var y=o.square(16);//y等於256

函式甚至不需要帶名字,當把它們賦值給陣列元素時:

var a=[function(x){return x*x;},20];//陣列直接量
a[0](a[1]);//=>400

最後一句程式碼看起來很奇怪,但的確是合法的函式呼叫表示式!

下面展示了將函式用做值時的一些例子,這段程式碼可能會難讀一些,但註釋解釋了程式碼的具體含義:

將函式用做值

//在這裡定義一些簡單的函式
function add(x,y){return x+y;}
function subtract(x,y){return x-y;}
function multiply(x,y){return x*y;}
function divide(x,y){return x/y;}//這裡的函式以上面的某個函式作為引數
//並給它傳入兩個運算元然後呼叫它

function operate(operator,operand1,operand2){
    return operator(operand1,operand2);
}

//這行程式碼所示的函式呼叫實際上計算了(2+3)+(4*5)的值
var i=operate(add,operate(add,2,3),operate(multiply,4,5));//我們為這個例子重複實現一個簡單的函式
//這次實現使用函式直接量,這些函式直接量定義在一個物件直接量中
var operators={
    add:function(x,y){return x+y;},
    subtract:function(x,y){return x-y;},
    multiply:function(x,y){return x*y;},
    divide:function(x,y){return x/y;},
    pow:Math.pow//使用預定義的函式
};//這個函式接收一個名字作為運算子,在物件中查詢這個運算子
//然後將它作用於所提供的運算元
//注意這裡呼叫運算子函式的語法

function operate2(operation,operand1,operand2){
    if(typeof operators[operation]==="function")
        return operators[operation](operand1,operand2);
    else throw"unknown operator";
}

//這樣來計算("hello"+""+"world")的值
var j=operate2("add","hello",operate2("add","","world"));//使用預定義的函式Math.pow()
var k=operate2("pow",10,2);

 這裡是將函式用做值的另外一個例子,考慮一下Array.sort()方法。這個方法用來對陣列元素進行排序。因為排序的規則有很多(基於數值大小、字母表順序、日期大小、從小到大、從大到小等),sort()方法可以接收一個函式作為引數,用來處理具體的排序操作。這個函式的作用非常簡單,對於任意兩個值都返回一個值,以指定它們在排序後的陣列中的先後順序。這個函式引數使得Array.sort()具有更完美的通用性和無限可擴充套件性,它可以對任何型別的資料進行任意排序。

  • 自定義函式屬性

JavaScript中的函式並不是原始值,而是一種特殊的物件,也就是說,函式可以擁有屬性。當函式需要一個“靜態”變數來在呼叫時保持某個值不變,最方便的方式就是給函式定義屬性,而不是定義全域性變數,顯然定義全域性變數會讓名稱空間變得更加雜亂無章。比如,假設你想寫一個返回一個唯一整數的函式,不管在哪裡呼叫函式都會返回這個整數。而函式不能兩次返回同一個值,為了做到這一點,函式必須能夠跟蹤它每次返回的值,而且這些值的資訊需要在不同的函式調過程中持久化。可以將這些資訊存放到全域性變數中,但這並不是必需的,因為這個資訊僅僅是函式本身用到的。最好將這個資訊儲存到函式物件的一個屬性中,下面這個例子就實現了這樣一個函式,每次呼叫函式都會返回一個唯一的整數:
 

//初始化函式物件的計數器屬性
//由於函式宣告被提前了,因此這裡是可以在函式宣告
//之前給它的成員賦值的
uniqueInteger.counter=0;//每次呼叫這個函式都會返回一個不同的整數
//它使用一個屬性來記住下一次將要返回的值
function uniqueInteger(){
    return uniqueInteger.counter++;//先返回計數器的值,然後計數器自增1
}

來看另外一個例子,下面這個函式factorial()使用了自身的屬性(將自身當做陣列來對待)來快取上一次的計算結果:
 

//計算階乘,並將結果快取至函式的屬性中
function factorial(n){
    if(isFinite(n)&&n>0&&n==Math.round(n)){//有限的正整數
        if(!(n in factorial))//如果沒有快取結果
            factorial[n]=n*factorial(n-1);//計算結果並快取之
        return factorial[n];//返回快取結果
    }
else return NaN;//如果輸入有誤
}
factorial[1]=1;//初始化快取以儲存這種基本情況

作為名稱空間的函式

JavaScript中的函式作用域的概念:在函式中宣告的變數在整個函式體內都是可見的(包括在巢狀的函式中),在函式的外部是不可見的。不在任何函式內宣告的變數是全域性變數,在整個JavaScript程式中都是可見的。在JavaScript中是無法宣告只在一個程式碼塊內可見的變數的,基於這個原因,我們常常簡單地定義一個函式用做臨時的名稱空間,在這個名稱空間內定義的變數都不會汙染到全域性名稱空間。

比如,假設你寫了一段JavaScript模組程式碼,這段程式碼將要用在不同的JavaScript程式中(對於客戶端JavaScript來講通常是用在各種各樣的網頁中)。和大多數程式碼一樣,假定這段程式碼定義了一個用以儲存中間計算結果的變數。這樣問題就來了,當模組程式碼放到不同的程式中執行時,你無法得知這個變數是否已經建立了,如果已經存在這個變數,那麼將會和程式碼發生衝突。解決辦法當然是將程式碼放入一個函式內,然後呼叫這個函式。這樣全域性變數就變成了函式內的區域性變數:

function mymodule(){//模組程式碼
//這個模組所使用的所有變數都是區域性變數
//而不是汙染全域性名稱空間
}
mymodule();//不要忘了還要呼叫這個函式

這段程式碼僅僅定義了一個單獨的全域性變數:名叫"mymodule"的函式。這樣還是太麻煩,可以直接定義一個匿名函式,並在單個表示式中呼叫它:

(function(){//mymodule()函式重寫為匿名的函式表示式
//模組程式碼
}());//結束函式定義並立即呼叫它

這種定義匿名函式並立即在單個表示式中呼叫它的寫法非常常見,已經成為一種慣用法了。注意上面程式碼的圓括號的用法,function之前的左圓括號是必需的,因為如果不寫這個左圓括號,JavaScript直譯器會試圖將關鍵字function解析為函式宣告語句。使用圓括號JavaScript直譯器才會正確地將其解析為函式定義表示式。使用圓括號是習慣用法,儘管有些時候沒有必要也不應當省略。這裡定義的函式會立即呼叫。