1. 程式人生 > >js基礎--函式屬性、方法和建構函式

js基礎--函式屬性、方法和建構函式

 

我們看到在JavaScript程式中,函式是值。對函式執行typeof運算會返回字串"function",但是函式是JavaScript中特殊的物件。因為函式也是物件,它們也可以擁有屬性和方法,就像普通的物件可以擁有屬性和方法一樣。甚至可以用Function()建構函式來建立新的函式物件。

  • length屬性

在函式體裡,arguments.length表示傳入函式的實參的個數。而函式本身的length屬性則有著不同含義。函式的length屬性是隻讀屬性,它代表函式實參的數量,這裡的引數指的是“形參”而非“實參”,也就是在函式定義時給出的實參個數,通常也是在函式呼叫時期望傳入函式的實參個數。

下面的程式碼定義一個名叫check()的函式,從另外一個函式給它傳入arguments陣列,它比較arguments.length(實際傳入的實參個數)和arguments.callee.length(期望傳入的實參個數)來判斷所傳入的實參個數是否正確。如果個數不正確,則丟擲異常。check()函式之後定義一個測試函式f(),用來展示check()的用法:

//這個函式使用arguments.callee,因此它不能在嚴格模式下工作
function check(args){
    var actual=args.length;//實參的真實個數
    var expected=args.callee.length;//期望的實參個數
    if(actual!==expected)//如果不同則丟擲異常
        throw Error("Expected"+expected+"args;got"+actual);
}
function f(x,y,z){
    check(arguments);//檢查實參個數和期望的實參個數是否一致
    return x+y+z;//再執行函式的後續邏輯
}
  • prototype屬性

每一個函式都包含一個prototype屬性,這個屬性是指向一個物件的引用,這個物件稱做“原型物件”(prototype object)。每一個函式都包含不同的原型物件。當將函式用做建構函式的時候,新建立的物件會從原型物件上繼承屬性。

  • call()方法和apply()方法

我們可以將call()和apply()看做是某個物件的方法,通過呼叫方法的形式來間接呼叫(見8.2.4節)函式(比如在例6-4我們使用了call()方法來呼叫一個物件的Objec t.prototype.toString方法,用以輸出物件的類)。call()和apply()的第一個實參是要呼叫函式的母物件,它是呼叫上下文,在函式體內通過this來獲得對它的引用。要想以物件o的方法來呼叫函式f(),可以這樣使用call()和apply():
f.call(o);
f.apply(o);
每行程式碼和下面程式碼的功能類似(假設物件o中預先不存在名為m的屬性)。
o.m=f;//將f儲存為o的臨時方法
o.m();//呼叫它,不傳入引數
delete o.m;//將臨時方法刪除
在ECMAScript 5的嚴格模式中,call()和apply()的第一個實參都會變為this的值,哪怕傳入的實參是原始值甚至是null或undefined。在ECMAScript 3和非嚴格模式中,傳入的null和undefined都會被全域性物件代替,而其他原始值則會被相應的包裝物件(wrapper object)所替代。

對於call()來說,第一個呼叫上下文實參之後的所有實參就是要傳入待呼叫函式的值。比如,以物件o的方法的形式呼叫函式f(),並傳入兩個引數,可以使用這樣的程式碼:

f.call(o,1,2);

apply()方法和call()類似,但傳入實參的形式和call()有所不同,它的實參都放入一個數組當中:

f.apply(o,[1,2]);

如果一個函式的實參可以是任意數量,給apply()傳入的引數陣列可以是任意長度的。比如,為了找出陣列中最大的數值元素,呼叫Math.max()方法的時候可以給apply()傳入一個包含任意個元素的陣列:
var biggest=Math.max.apply(Math,array_of_numbers);
需要注意的是,傳入apply()的引數陣列可以是類陣列物件也可以是真實陣列。實際上,可以將當前函式的arguments陣列直接傳入(另一個函式的)apply()來呼叫另一個函式,參照如下程式碼:


//將物件o中名為m()的方法替換為另一個方法
//可以在呼叫原始的方法之前和之後記錄日誌訊息
function trace(o,m){
    var original=o[m];//在閉包中儲存原始方法
    o[m]=function(){//定義新的方法
        console.log(new Date(),"Entering:",m);//輸出日誌訊息
        var result=original.apply(this,arguments);//呼叫原始函式
        console.log(new Date(),"Exiting:",m);//輸出日誌訊息
        return result;//返回結果
    };
}
trace()函式接收兩個引數,一個物件和一個方法名,它將指定的方法替換為一個新方法,這個新方法是“包裹”原始方法的另一個泛函數[14]。這種動態修改已有方法的做法有時稱做"monkey-patching"。
  • bind()方法

bind()是在ECMAScript 5中新增的方法,但在ECMAScript 3中可以輕易模擬bind()。從名字就可以看出,這個方法的主要作用就是將函式繫結至某個物件。當在函式f()上呼叫bind()方法並傳入一個物件o作為引數,這個方法將返回一個新的函式。(以函式呼叫的方式)呼叫新的函式將會把原始的函式f()當做o的方法來呼叫。傳入新函式的任何實參都將傳入原始函式,比如:


function f(y){return this.x+y;}//這個是待繫結的函式
var o={x:1};//將要繫結的物件
var g=f.bind(o);//通過呼叫g(x)來呼叫o.f(x)
g(2)//=>3
可以通過如下程式碼輕易地實現這種繫結:


//返回一個函式,通過呼叫它來呼叫o中的方法f(),傳遞它所有的實參
function bind(f,o){
    if(f.bind)return f.bind(o);//如果bind()方法存在的話,使用bind()方法
    else return function(){//否則,這樣繫結
        return f.apply(o,arguments);
    };
}
ECMAScript 5中的bind()方法不僅僅是將函式繫結至一個物件,它還附帶一些其他應用:除了第一個實參之外,傳入bind()的實參也會繫結至this,這個附帶的應用是一種常見的函數語言程式設計技術,有時也被稱為“柯里化”(currying)。參照下面這個例子中的bind()方法的實現:


var sum=function(x,y){return x+y};//返回兩個實參的和值
//建立一個類似sum的新函式,但this的值繫結到null
//並且第一個引數繫結到1,這個新的函式期望只傳入一個實參
var succ=sum.bind(null,1);
succ(2)//=>3:x繫結到1,並傳入2作為實參y
function f(y,z){return this.x+y+z};//另外一個做累加計算的函式
var g=f.bind({x:1},2);//繫結this和y
g(3)//=>6:this.x繫結到1,y繫結到2,z繫結到3
我們可以繫結this的值並在ECMAScript 3中實現這個附帶的應用。例8-5中的示例程式碼就模擬實現了標準的bind()方法。

注意,我們將這個方法另存為Function.prototype.bind,以便所有的函式物件都繼承它,這種技術在9.4節中有詳細介紹:

例8-5:ECMAScript 3版本的Function.bind()方法


if(!Function.prototype.bind){
    Function.prototype.bind=function(o/*,args*/){//將this和arguments的值儲存至變數中
//以便在後面巢狀的函式中可以使用它們
        var self=this,boundArgs=arguments;//bind()方法的返回值是一個函式
        return function(){//建立一個實參列表,將傳入bind()的第二個及後續的實參都傳入這個函式
            var args=[],i;
            for(i=1;i<boundArgs.length;i++)args.push(boundArgs[i]);
            for(i=0;i<arguments.length;i++)args.push(arguments[i]);//現在將self作為o的方法來呼叫,傳入這些實參
            return self.apply(o,args);
        };
    };
}
我們注意到,bind()方法返回的函式是一個閉包,在這個閉包的外部函式中聲明瞭self和boundArgs變數,這兩個變數在閉包裡用到。儘管定義閉包的內部函式已經從外部函式中返回,而且呼叫這個閉包邏輯的時刻要在外部函式返回之後(在閉包中照樣可以正確訪問這兩個變數)。

ECMAScript 5定義的bind()方法也有一些特性是上述ECMAScript 3程式碼無法模擬的。首先,真正的bind()方法返回一個函式物件,這個函式物件的length屬性是繫結函式的形參個數減去繫結實參的個數(length的值不能小於零)。再者,ECMAScript 5的bind()方法可以順帶用做建構函式。如果bind()返回的函式用做建構函式,將忽略傳入bind()的t his,原始函式就會以建構函式的形式呼叫,其實參也已經繫結[15]。由bind()方法所返回的函式並不包含prototype屬性(普通函式固有的prototype屬性是不能刪除的),並且將這些繫結的函式用做建構函式時所建立的物件從原始的未繫結的建構函式中繼承prototype。同樣,在使用instanceof運算子時,繫結建構函式和未繫結建構函式並無兩樣。
  • toString()方法

和所有的JavaScript物件一樣,函式也有toString()方法,ECMAScript規範規定這個方法返回一個字串,這個字串和函式宣告語句的語法相關。實際上,大多數(非全部)的toString()方法的實現都返回函式的完整原始碼。內建函式往往返回一個類似"[native code]"的字串作為函式體。

  • Function()建構函式

不管是通過函式定義語句還是函式直接量表達式,函式的定義都要使用function關鍵字。但函式還可以通過Function()建構函式來定義,比如:

var f=new Function("x","y","return x*y;");
這一行程式碼建立一個新的函式,這個函式和通過下面程式碼定義的函式幾乎等價:

var f=function(x,y){return x*y;}
Function()建構函式可以傳入任意數量的字串實參,最後一個實參所表示的文字就是函式體;它可以包含任意的JavaScript語句,每兩條語句之間用分號分隔。傳入建構函式的其他所有的實參字串是指定函式的形參名字的字串。如果定義的函式不包含任何引數,只須給建構函式簡單地傳入一個字串——函式體——即可。

注意,Function()建構函式並不需要通過傳入實參以指定函式名。就像函式直接量一樣,Function()建構函式建立一個匿名函式。

關於Function()建構函式有幾點需要特別注意:

·Function()建構函式允許JavaScript在執行時動態地建立並編譯函式。

·每次呼叫Function()建構函式都會解析函式體,並建立新的函式物件。如果是在一個迴圈或者多次呼叫的函式中執行這個建構函式,執行效率會受影響。相比之下,迴圈中的巢狀函式和函式定義表示式則不會每次執行時都重新編譯。

·最後一點,也是關於Function()建構函式非常重要的一點,就是它所建立的函式並不是使用詞法作用域,相反,函式體程式碼的編譯總是會在頂層函式[16]執行,正如下面程式碼所示:


var scope="global";
function constructFunction(){
    var scope="local";
    return new Function("return scope");//無法捕獲區域性作用域
}
//這一行程式碼返回global,因為通過Function()建構函式
//所返回的函式使用的不是區域性作用域
constructFunction()();//=>"global"
我們可以將Function()建構函式認為是在全域性作用域中執行的eval()(參照4.12.2節),eval()可以在自己的私有作用域內定義新變數和函式,Function()建構函式在實際程式設計過程中很少會用到