JavaScript權威指南--函數

要點概述

 

函數是這樣一段代碼,它只定義一次,但可能被執行或調用任意次。javascript

javascript函數是參數化的:函數定義會包括一個形參(parmeter)標識符列表。這些參數在函數中像局部變量同樣工做。函數調用會給形參提供實參的值。函數使用它們實參的值計算返回值,成爲該函數的調用表達式的值。除了實參以外,每次調用還會擁有一個值——本地調用的上下文——這就是this關鍵字值。java

若是函數掛載在一個對象上,做爲對象的一個屬性,就稱爲它爲對象的方法。當經過這個對象來調用函數時,該對象就是這次調用的上下文(context),也就是該函數的this值。用於初始化一個新建立對象的函數稱爲構造函數(constructor)。程序員

在javascript中,函數即對象,程序可隨意操做它們。好比,javascript能夠把函數賦值給變量,或者做爲參數傳遞給其餘函數。由於函數就是對象,因此能夠給他們設置屬性,甚至調用它們的方法。web

javascript的函數能夠嵌套在其餘函數中定義,這樣他們就能夠訪問它們被定義時所處的做用域變量。這意味着javascript函數構成了一個閉包(closere),它給javascript帶來了很是強勁的編程能力。編程

1.函數定義

函數使用function關鍵字來定義。它能夠用在函數定義表達式或者函數聲明語句裏。在這兩種形式中,函數定義都從function關鍵字開始,其後跟隨這些部分:數組

  1. 函數名稱標識符:函數名稱是函數聲明語句必須的部分。它的用途就像是變量的名字,新定義的函數對象會賦值給這個變量。對函數定義表達式來講,這個名字是可選的:若是存在,該名字只存在函數中,並代指函數對象自己。
  2. 一對圓括號:其中包含由0個或者多個逗號隔開的標識符組成的列表。這些標識符是函數的參數名稱,它們就像函數體中的局部變量同樣。
  3. 一對花括號,裏邊包含0條或者多條javascript語句。這些語句構成了函數體:一旦調用函數,就會執行這些語句。
 //定義javascript函數
 //輸出o的每一個屬性的名稱和值,返回undefined
function printprops(o) {
    for (p in o)
        console.log(p + ":" + o[p] + "\n")
}

 //計算兩個迪卡爾座標(x1,y1)和(x2,y2)之間的距離
function distance(x1, y1, x2, y2) {
    var dx = x2 - x1;
    var dy = y2 - y1;
    return Math.sqrt(dx * dx + dy * dy)
}

 //計算遞歸函數(調用自身的函數)
 //x!的值是從x到x遞減(步長爲1)的值的累乘
function factorial(x) {
    if (x <= 1) return 1;
    return x * factorial(x - 1);
}

 //這個函數表達式定義了一個函數用來求傳入參數的平方
 //注意咱們把它賦值了給一個變量
var square = function(x) {
    return x * x
}

 //函數表達式能夠包含名稱,這在遞歸時頗有用
var f = function fact(x) {
    if (x <= 1) return 1;
    else return x * fact(x - 1);
};
 //f(7)=>5040

 //函數表達式也能夠做爲參數傳給其它函數
data.sort(function(a, b) {
    return a - b;
});

 //函數表達式有時定義後當即使用
var tensquared = (function(x) {
    return x * x;
}(10))

注意:以表達式定義的函數,函數的名稱是可選的。一條函數聲明語句實際上聲明瞭一個變量。並把一個函數對象賦值給它。相對而言,定義函數表達式時並無聲明一個變量。函數能夠命名,就像上面的階乘函數,它須要一個名稱來指代本身。瀏覽器

若是一個函數定義表達式包含名稱,函數的局部變量做用域將會包含一個綁定到函數對象的名稱。實際上,函數的名稱將成爲函數內部的一個局部變量。一般而言,以表達式方式定義函數時不須要名稱,這會讓定義它們的代碼更緊湊。函數定義表達式特別適合用來那些只用到一次的函數,好比上面展現的最後兩個例子。緩存

函數聲明提早。函數表達式就不同了,賦給一個變量後變量聲明提早了,可是函數沒法調用。閉包

return語句致使函數中止執行。並返回它的表達式(若是有的話)的值給調用者。若是return語句沒有一個與之相關的表達式,則它返回undefined值。若是一個函數不包含return語句。那它就執行函數體內的每條語句,並返回undefined值給調用者。app

嵌套函數

function hyuse(a, b) {
    function square(x) {
        return x * x
    }
    return Math.sqrt(square(a) + square(b));
}

嵌套函數的有趣之處在於它的變量做用域規則:它們能夠訪問嵌套它們(或者多重嵌套)的函數的參數和變量。

函數聲明語句並不是真正的語句。ECMAScript規範只是容許它們做爲頂級語句,它們能夠出如今全局代碼裏,或者內嵌在其餘函數中,但它們不能出如今循環、條件判斷、或者try/cache/finally及with語句中(有些javascript併爲嚴格遵循這條規則,好比Firefox就容許在if語句中出現條件函數聲明)。注意:此限制僅適用於以語句形式定義的函數。函數定義表達式能夠出如今javascript的任何地方。

 2.函數調用

構成函數主題的javascript代碼在定義之時並不會執行,只有調用該函數是,它們纔會執行。有4種方式來調用javascript函數。

  • 做爲函數
  • 做爲方法
  • 做爲構造函數
  • 經過它們的call()或apply()方法間接調用

2.1.函數調用

使用調用表達式能夠進行普通的函數調用也能夠進行方法調用。一個調用表達式由多個函數表達式組成,每一個函數表達式都是由一個函數對象和左圓括號、參數列表和右圓括號組成,參數列表是由逗號分隔的逗號的零個或多個參數表達式組成。若是函數表達式是一個屬性訪問表達式,即該函數是一個對象的屬性或數組中的一個元素。那麼它就是一個方法調用表達式。

在一個調用中,每一個參數表達式(圓括號之間的部分)都會計算出一個值,計算的結果做爲參數傳遞給另一個函數。這些值做爲實參傳遞給聲明函數時定義的形參。在函數體中存在一個形參的調用,指向當前傳入的實參列表,經過它能夠得到參數的值。

對於普通的函數調用,函數的返回值成爲調用表達式的值。若是該函數返回是由於解釋器到達結尾,返回值就是undefined。若是函數返回是由於解釋器執行到一條return語句,返回的值就是return以後的表達式值,若是return語句沒有值,則返回undefined。

根據ECMAScript3和非嚴格的ECMAScript5對函數的調用規定,調用上下文(this的值)是全局對象。而後在嚴格模型下,調用上下文則是undefined、
以函數的形式調用的函數一般不使用this關鍵字。不過 ,「this」能夠用來判斷當前是否爲嚴格模式。

//定義並調用一個函數來肯定當前腳本運行是否爲嚴格模式
var strict = (function() {return !this;}())

2.2.方法調用

一個方法無非是個保存在一個對象的屬性裏的javascript函數。若是有一個函數f和一個對象o,則能夠用下面的代碼給o定義一個名爲m()的方法:o.m = f;

對方法調用的參數和返回值的處理,和上面所描述的普通函數調用徹底一致。可是方法調用和函數調用有一個重要的區別,即:調用上下文。屬性訪問表達式由兩部分組成:一個對象(本例中的o)和屬性名稱(m)。像這樣的方法在調用表達式裏,對象o成爲調用上下文,函數體可使用關鍵字this引用該對象。

大多數方法調用使用點符號來訪問屬性,使用方括號(的屬性訪問表達式)也能夠進行屬性訪問操做。下面兩個例子都是函數的調用:

o["m"](x,y) //o.m(x,y)的另一種寫法
a[0](z)//一樣是一個方法調用(這裏假設a[0]是一個函數)

方法調用可能包含更復雜的函數屬性訪問表達式:

customer.surname.toUpperCase(); //調用customer.surname方法
f().m(); //在f()調用結束後繼續調用返回值中的方法m()

方法和this關鍵字是面向對象編程範例的核心。任何函數只要做爲方法調用實際上都會傳入一個隱式的實參——這個實參是一個對象,方法調用的母體就是這個對象。


方法鏈

當方法的返回值是一個對象,這個對象還能夠再調用它的方法。這種方法調用序列中(一般稱爲「鏈」或者「級聯」)每次的調用結果都是另一個表達式組成部分。好比基於jQuery(19章會講到),咱們常這樣寫代碼:

//找到全部的header,取得他們的id的映射,轉換爲數組並給它們進行排序
$(":header").map(function(){return this.id}).get().sort();

當方法並不須要返回值時,最好直接返回this。若是在設計的API中一直採用這種方式(每一個方法都返回this),使用API就能夠進行「鏈式調用」風格的編程,在這種編程風格中,只要指定一次要調用的對象便可。餘下的方法都看一基於此進行調用:

shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();

不要講方法的鏈式調用和構造函數的鏈式調用混爲一體。

鏈式調用:函數式編程

const utils = {
  chain(a) {
    this._temp = a;
    return this;
  },
  sum(b) {
    this._temp += b;
    return this;
  },
  sub(b) {
    this._temp -= b;
    return this;
  },
  value() {
    const _temp = this._temp;
    this._temp = undefined;
    return _temp;
  }
};

console.log(utils.chain(1).sum(2).sum(3).sub(4).value());

 


須要注意的是,this是一個關鍵字,不是變量,也不是屬性名。javascript的語法不容許給this賦值。

和變量不一樣,關鍵字this沒有做用域的限制,嵌套的函數不會從調用它的函數中繼承this。若是嵌套函數做爲方法調用,其this的值只想調用它的對象。若是嵌套函數做爲函數調用,其this值不是全局對象(非嚴格模式下)就是undefined(嚴格模式下)。不少人誤覺得調用嵌套函數時this會指向調用外層函數的上下文。若是你想訪問這個外部函數的this值,須要將this值保存在一個變量裏,這個變量和內部函數都在一個做用域內。一般使用變量self來保存this。好比:

var o = { //對象o
  m: function() { //對象中的方法m()
    var self = this; //將this的值保存在一個變量中
    console.log(this === o); //輸出true,this就是這個對象o
    f(); //調用輔助函數f()

    function f() { //定義一個嵌套函數f()
      console.log(this === o); //"false":this的值是全局對象undefied
      console.log(self === o); //"true": slef指外部函數this的值
    }
  }
};
o.m();//調用對象o的方法m

2.3.構造函數調用

若是函數或者方法以前帶有關鍵字new,它就構成構造函數調用(構造函數掉在4.6節和6.1.2節有簡單介紹,第9章會對構造函數作更詳細的討論)。構造函數調用和普通的函數調用方法以及方法調用在實參處理、調用上下文和返回值各方面都不一樣。

若是構造函數調用圓括號內包含一組實參列表,先計算這些實參表達式,而後傳入函數內,這和函數調用和方法調用是一致的。但若是構造函數沒有形參,javascript構造函數調用的語法是容許省略形參列表和圓括號的。以下文兩個代碼是等價的:

var o = Object();
var o = Object;

構造函數調用建立一個新的 空對象,這個對象實例繼承自構造函數prototype屬性。構造函數試圖初始化這個新建立的對象,並將這個對象用作其調用上下文。所以,構造函數可使用this關鍵字來引用這個新建立的對象。

注意:儘管構造函數看起來像一個方法調用,它依然會使用這個新對象做爲調用上下文。也就是說,在表達式new o.m()中,調用上下文並非o。

構造函數一般不使用return關鍵字,它們一般初始化新對象,當構造函數的函數體執行完畢時,它顯式返回。這種狀況下,構造函數調用表達式的計算結果就是這個新對象的值。然而,若是構造函數顯式的使用了return語句返回一個對象,那麼調用表達式的值就是這個對象。若是構造函數使用return語句但沒有指定返回值。或者返回一個原始值,那麼這時將忽略返回值。同時使用這個新對象做爲調用結果。

2.4.間接調用

javascript中的函數也是對象,和其它javascript對象沒有什麼兩樣,函數對象也能夠包含方法。其中的兩個方法call()和apply()能夠用來間接的調用函數。兩個方法都容許間接的調用函數。兩個方法都容許顯式指定調用所需的this值,也就是說,任何函數能夠做爲任何對象的方法來調用,哪怕這個函數不是那個對象的方法。

3.函數的實參和形參

javascript中的函數定義並未指定函數的形參類型,函數調用也未對實參作任何類型的檢測。實際上javascript甚至不檢查傳入的形參的個數。下面幾節將會討論當調用函數時實參個數和聲明的形參個數不匹配時出現的情況。一樣說明了如何顯式測試函數實參的類型避免非法的實參傳入函數

3.1可選參數

當調用函數的時候傳入的實參比函數聲明時指定的形參個數要少,剩下的的形參都將設置爲undefined值。所以,在調用函數時,形參是否可選以及是否能夠省略應當保持 較好適應性。爲了作到這一點,應當給省略的參數賦一個合理的默認值、來看這個例子:

//將對象o中的可枚舉屬性名追加到數組a中,並返回這個數組a
//若是省略a,則建立一個新數組並返回這個新數組
function getPropertyNames(o, /*optional*/ a) {
if (a === undefined) a = []; //若是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 || []。

須要注意的是,使用「||」運算符代替if語句的前提是a必須先聲明,不然表達式會報引用錯誤,在這個例子中a是做爲形參傳入的,至關於var a,既然已經聲明a,因此這樣用是沒有問題的。

須要注意的是,當用這種可選實參來實現函數時,須要將可選實參放在參數列表的最後。那行調用你的函數的人是沒辦法省略第一個實參傳入第二個實參的它必須將undefined顯式傳入【注意:函數的實參可選時每每傳入一個無心義的佔位符,慣用的作法是傳入null做爲佔位符,固然也可使用undefined】,一樣要注意在函數定義中,使用註釋/*optional*/來強調形參是可選的。

3.2.可變長的實參列表:實參對象

當調用函數的時候,傳入的實參的個數大於函數定義的形參個數時,沒有辦法得到未命名值的引用。參數對象解決了這個問題。在函數體內,arguments是指向實參對象的引用,實參對象是一個類數組的對象(參照7章11節),這樣能夠經過數字下標就能訪問傳入函數的實參值。而不用非要經過名字來獲得實參

假設定義函數f,它只有一個實參x。若是調用這個函數時須要傳入兩個實參,第一個實參能夠經過參數名x來得到,也能夠經過arguments[0]來獲得。第二個實參只能經過arguments[1]來獲得。此外和真正的數組同樣,arguments也包含一個length屬性,用以表示其所包含元素的個數。所以,調用函數f()時傳入兩個參數,arguments.length的值就是2.

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

須要注意的是,一般沒必要這樣檢查實參個數。大多數狀況下,javascript的默認行爲能夠知足須要的:省略的實參都是undefined,多出的實參會自動省略。

實參對象有一個重要的用處,就是讓函數操做任意數量的實參。以下的不定實參函數

function max( /*...*/ ) {
    var max = Number.NEGATIVE_INFINITY;
    //遍歷實參,查找並記住最大值
    for (var i = 0; i < arguments.length; i++)
        if (arguments[i] > max) max = arguments[i];
        //返回最大值
    return max;
}
max(10, 100, 22, 1000); //=>1000

注意:不定實參函數的實參個數不能爲零。arguments[]對象最適合的場景是在這樣一類函數中,這類函數包含固定個數的命名和必須參數,以及隨後個數不定的可選實參。

記住,arguments並非真正的數組。它是一個實參對象,碰巧有以數字索引的屬性。

數組對象包含一個非同尋常的特性。在非嚴格模式下,當一個函數包含若干形參,實參對象的數組元素是函數形參所對應實參別名,實參對象以數字索引,實參對象中以數字索引,而且形參名稱能夠能夠認爲是相同變量的不一樣命名。經過實參名字來修改實參值的話,經過arguments[]數組也能夠獲取到更改後的值,下面的這個例子清楚的說明了這一點。

function f(x) {
    console.log(x); //輸出實參的初始值
    arguments[0] = null; //修改實參組的元素一樣會修改x的內容
    console.log(x); //輸「null」
}

若是實參對象是一個普通的數組的話,第二條console.log(x)語句結果絕對不是null.這個例子中,arguments[]和x指代同一個值。

在ECMAScript5中移除了實參對象的這個特殊屬性。在嚴格模型下還有一點(和非嚴格模式不一樣),在非嚴格模式下,函數裏的arguments僅僅是一個標識符,在嚴格模式中,它變成了一個保留字。嚴格模式下 函數沒法使用arguments做爲形參名或局部變量名,也不能給arguments賦值。

callee和caller屬性

除了數組元素,實參對象還定義了callee和caller屬性。在非嚴格模式下(嚴格模式下會有一系列錯誤),ECMAScript標準規範規定callee屬性指代當前正在執行的函數。caller屬性是非標準的,但大多數瀏覽器都實現這個屬性。它指代調運當前正在執行的函數的函數。經過方法caller屬性能夠訪問調運棧。callee屬性在某些時候很是有用,好比在匿名函數中經過callee來遞歸調用自身。

var factorial = function(x) {
    if (x <= 1) return 1;
    return x * arguments.callee(x - 1);
}

區別:

//1 :caller 返回一個調用當前函數的引用 若是是由頂層調用的話 則返回null
    var callerTest = function() {
        console.log(callerTest.caller) ;  
    } ;
    function a() {
        callerTest() ;   
    }
    a() ;//輸出function a() {callerTest();}
    callerTest() ;//輸出null 

//2 :callee 返回一個正在被執行函數的引用  (這裏經常使用來遞歸匿名函數自己 可是在嚴格模式下不可行)
//callee是arguments對象的一個成員 表示對函數對象自己的引用 它有個length屬性(表明形參的長度)
    var c = function(x,y) {
        console.log(arguments.length,arguments.callee.length,arguments.callee)
    } ;
    c(1,2,3) ;//輸出3 2 function(x,y) {console.log(arguments.length,arguments.callee.length,arguments.callee)} 

3.3.將對象屬性用做實參

當一個函數包含超過3個形參時,對於程序員來講,要記住調用函數中實參的正確順序實在讓人頭疼。每次調用這個函數時都不厭其煩的查閱文檔,爲了避免讓程序員每次都要梳理,最好經過名/值對的形式傳入參數。這樣參數的順序就可有可無了。爲了實現這樣風格的方法調用,定義函數的時候,傳入的實參都寫入一個單獨的對象之中,在調用的時候傳入一個對象,對象中的名/值纔是真正須要的實參數據,以下例子,這樣的寫法容許在函數中設置省略參數的默認值。

 //將原始值數組的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.form,
            args.form_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
});

3.4.實參類型

對其進行類型檢測。

能夠添加註釋、類型轉換、代碼檢測

。。。。。。

4.做爲值的函數

在javascript中,函數不只是一種語法,也是值。也就是說,能夠將函數賦值給變量。存儲在對象的屬性或數組的元素中,做爲參數傳入另一個函數等。

爲了便於理解javascript中的函數是如何作數據的以及javascript語法,來看一個函數定義:

function square(x) {
    return x * x
}

這個定義建立一個新的函數對象,並將其賦值給square。函數的名字其實是看不見的,它(square)僅僅是變量的名字。這個變量指代函數對象。函數還能夠賦值給其它的變量,而且仍能夠正常工做:

var s = square; //如今s和sqare指代同一個函數
square(4); //=>16
s(4); //=>16

除了能夠將函數賦值給變量,統一能夠將函數賦值給對象的屬性。當函數做爲對象的屬性調用時,函數就稱爲方法。

var o = {
    square: function(x) {return x * x}
}; //對象直接量
var y = o.square(16);

函數甚至不須要名字,當把他們賦值給數組元素時:

var a = [function(x) {return x * x},20];
console.log(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) {returnx / y},
    pow:Math.pow()//使用預約義的函數
};
//這個函數接受一個名字做爲運算符,在對象中查找這個運算符
//而後將它做用於鎖提供的操做數
//注意這裏調用運算符函數語法
function operate2(operation,operand1,operand2){
    if(typeof operators[operation] === "function")
    return operators[operation](operand1,operand2);
    else throw "unkown operators";
}
//這樣來計算("hello" + "" + "world")的值
var j = operate2("add","hello",operate2("add","","world") );
//使用預約義的函數Math.pow()
var k = operate2("pow",10,2);

這裏是將函數作值的另一個例子,考慮下Array.sort()方法sort()方法能夠接受一個函數做爲參數,用來處理具體的排序操做。

自定義函數屬性

javascript中的函數並非原始值,而是一種特殊的對象,也就是說,函數能夠擁有屬性。當函數須要一個「靜態」的變量來調用時保持某個值不變,最方便的方法就是給函數定義屬性,而不是全局變量。顯然定義全局變量會讓命名空間變得更雜亂無章。
好比:你想寫一個返回一個惟一整數的函數,無論在哪裏調用的函數都會返回這個整數。而函數不能兩次返回同一個值。爲了作到這一點,函數必須可以跟蹤它每次返回的值,並且這些值的信息須要在不一樣函數調用過程當中持久化。能夠將這些信息存放到全局變量中,但這並非必需的,由於這個信息僅僅是函數自己用到的。最好將這個信息保存到函數的一個屬性中,下面這個例子就實現了這樣的一個函數,每次調用函數都會返回一個惟一的整數:

//初始化函數對象的計數器屬性
//因爲函數聲明被提早了,所以這個是能夠在函數聲明
//以前給它的成員賦值的
unInterger.counter = 0;

//每次調用這個函數都會返回一個不一樣的整數
//它使用一個屬性來記住下一次將要返回的值
function unInterger() {
  unInterger.counter++  ; //先返回計數器的值,而後計數器自增1
}

來看另一個例子,下面這個函數factorrial()使用了自身屬性(將自身當作數組來對待)來緩存上一次的計算結果:

 //計算階乘,並將結果緩存在函數的屬性中
function factorrial(n) {
    if (isFinite(n) && n > 0 && n == Math.round(n)) { //有限的正整數
        if (!(n in factorrial)) //若是沒有緩存結果
            factorrial[n] = n * factorrial(n - 1); //計算並緩存之
        return factorrial[n];
    } else return NaN; //若是輸入有誤
}
factorrial[1] = 1; //初始化緩存以保存這種基本狀況
console.log(factorrial(5))

5.做爲命名空間的函數

javascript中是沒法聲明只在一個代碼塊內可見的變量的(在客戶端javascript中這種說法不徹底正確,在有些javascript擴展中就可使用let聲明語句塊內的變量,詳細內容見11章),基於這個緣由,咱們經常簡單定義一個函數用作臨時命名空間,在這個命名空間內定義的變量不會污染的全局命名空間。

好比,假設你寫了一段javascript模塊代碼,這段代碼將要用在不一樣的javascript程序中,會出現已經存在的變量和代碼發生衝突。解決的辦法固然是將代碼放入一個函數內,而後調用這個函數。這樣全局變量就編程了函數內的局部變量:

function mymodule() {
    //模塊代碼
    //這個模塊全部使用的全部變量是局部變量
    //而不是污染全局命名空間
}
mymodule(); //不要忘了還要調用的這個函數

這段代碼僅僅定義了一個單獨的全局變量,名叫「mymodule」的函數。這樣仍是太麻煩了,能夠直接定義一個匿名函數,並在單個表達式中調用它:

(function() { //mymodule函數重寫爲匿名函數表達式
    //模塊代碼
}()); //結束函數定義並當即調用它

function以前的左括號是必須的,由於若是不寫這個左圓括號,javascript解釋器會試圖將其解析爲函數定義表達式。使用了它javascript解釋器纔會正確地將其解析爲函數定義表達式。

例子展現了這種命名空間技術

。。。。。

6.閉包

和大多數現代編程語言同樣,javascript也採用詞法做用域(lexical scoping),也就是說,函數的執行依賴於變量做用域,這個做用域是在函數定義時決定的,而不是函數調用時決定的。

爲了實現這種詞法做用域,javascript函數對象的內部狀態不只包含函數的代碼邏輯,還必須引用當前的做用域。

函數對象能夠經過做用域相互關聯起來,函數體內部的變量均可以保持在函數的做用域內,這種特性在計算機科學文獻中稱爲「閉包」。(這種叫法很是古老,是指函數的變量能夠隱藏於做用域鏈以內,所以看起來是函數將變量包裹了起來。)

調用函數時閉包所指向的做用域鏈和定義函數時的做用域鏈不是同一個做用域鏈。

理解閉包首先須要瞭解嵌套函數的詞法做用域規則,看一下這段代碼

var scope = "global scope"; //全局變量
function checkscope() {
    var scope = "local scope"; //局部變量
    function f() {
            return console.log(scope);
        } //在做用域中返回這個值
    return f();
}
checkscope(); // local scope時

改動:

var scope = "global scope"; //全局變量
function checkscope() {
    var scope = "local scope"; //局部變量
    function f() {
            return console.log(scope);
        } //在做用域中返回這個值
    return f;
}
checkscope()(); // 

checkscope()如今僅僅返回函數內嵌套的一個函數對象,而不是直接返回結果。

回想一下這個詞法做用域的基本規則:javascript函數的執行用到了做用域鏈。這個做用域鏈是函數定義的時候建立的。嵌套的函數f()定義在這個做用域鏈裏,其中的變量scope必定是局部變量,無論在什麼時候何地都執行函數f(),這種綁定在執行f()時依然有效。所以,最後一行返回"local scope",而不是「global」.簡言之,閉包的這個特性強大到讓人吃驚:它能夠捕捉到局部變量(和參數),並一直保存下來,看起來像這些變量綁定到在其中定義他們的外部函數。

實現閉包

咱們將做用域鏈描述爲一個對象列表,不是綁定的棧。每次調用javascript函數的時候,都會爲之建立一個新的對象用來保存局部變量,把這個對象添加至做用域鏈中。當函數返回的時候,就從做用域鏈中將這個綁定的變量的對象刪除。若是不存在嵌套的函數,也沒有其它引用指向這個綁定的對象,它就會被當作垃圾回收掉。若是定義了嵌套的函數,每一個嵌套的函數都各自對應一個做用域鏈,而且這個做用域鏈指向一個變量綁定對象。但若是這些嵌套的函數對象在外部函數中保留了下來,那麼它們也會和所指向的變量綁定對象同樣當作垃圾回收。可是若是這個函數定義了嵌套函數,並將它做爲返回值返回或者存儲在某處的屬性裏,這時就會有一個外部引用指向這個嵌套的 函數,它就不會被當作垃圾回收,而且它所指向的變量綁定也不會被當作垃圾回收(做者在這裏清楚地解釋了閉包和垃圾回收以前的關係,若是使用不慎,閉包很容易形成「循環引用」,當DOM對象和javascript對象以前存在循環引用時須要格外當心,在某些瀏覽器下會形成內存泄漏)。

前面定義的unInterger()函數,這個函數使用自身的一個屬性來保存每次返回的值,以便每次調用都能跟蹤上次的返回值。可是這種作法有一個問題,就是惡意代碼可能將計數器重置或者把一個非整數賦值給它,致使unInterger()函數不必定能產生「惟一」的「整數」。而閉包能夠捕捉到單個函數調用的局部變量,並將這些局部變量用作私有狀態,咱們能夠利用閉包重寫這個函數:

var unInterger = (function() { //定義函數並當即調用
    var counter = 0; //函數的私有狀態
    return function() {return counter++;};
}());

像counter同樣的私有變量不是隻能用在一個單獨的閉包內,在同一個外部函數內定義多個嵌套函數能夠訪問它,這個嵌套函數都共享一個做用域鏈,看一下這短代碼:

function counter(){
    var n =0;
    return{
        count:function(){return n++;},
        reset:function(){n = 0;}
    };
}
var c = counter(),d = counter(); //建立兩個計數器
console.log(c.count())        //=>0
console.log(d.count())        //=>0
console.log(c.reset())        // reset()和count方法共享狀態 undefined
console.log(c.count())        //=>0 由於咱們重置了c
console.log(d.count())      //=>1 咱們沒有重置d
console.log(d.count())      //=>2 

每次調用counter()會建立一個新的做用域鏈和一個新的私有變量。所以,若是調用counter()兩次會獲得兩個計數器對象,並且彼此包含不一樣的私有變量,調用其中一個計數器對象的count()或者reset()不會影響另一個對象。

從技術角度看,其實能夠將這個閉包合併爲屬性存取器方法,getter和setter.下面這段代碼所示的counter()函數是6章6節中代碼的變種,所不一樣的是,這裏私有狀態的實現是利用了閉包,而不是利用普通的對象屬性來實現:

function counter(n) { //函數參數n是一個私有變量
    return {
        //屬性getter方法返回並給私有計數器var遞增1
        get count() {
                return n++;
            },
            //屬性setter方法不容許n遞減
            set count(m) {
                if (m >= n) n = m;
                else throw Error("count can only be set to a larger value");
            }
    };
}
var c = counter(1000);
console.log(c.count) //=>1000
console.log(c.count) //=>1001
console.log(c.count) //=>1002
console.log(c.count = 2000) 
console.log(c.count) //=>2000
console.log(c.count) //=>2001
console.log(c.count = 2000) //Error: count can only be set to a larger value

須要注意的是,這個版本的counter()函數並未聲明局部變量,而只是使用參數n來保存私有狀態,屬性存取器方法能夠訪問n。這樣的話,調用counter()的函數就能夠指定私有變量的初始值了。

下面的這個例子,利用閉包技術來共享私有狀態的通用作法。這個例子定義了一個addPrivateProperty()函數,這個函數定義了一個私有變量,以及兩個嵌套的函數來獲取和設置這個私有變量的值。它將這些嵌套函數添加爲所指定對象的方法。

。。。。。。

咱們已經給出了不少例子,在同一個做用域鏈中定義兩個閉包,這兩個閉包共享一樣的私有變量或變量。這是一種很是重要的技術,但仍是要當心那些不但願共享的變量每每不經意間共享給了其它的閉包,瞭解這一點很是重要。看一下下面的這段代碼:

 //這個函數返回一個老是返回v的函數
function constfunc(v) {
    return function() {return v;}
};

 //建立一個數組用來常數函數
var funcs = [];
for (var i = 0; i < 10; i++) funcs[i] = constfunc(i);

 //在第5個位置的元素所表示的函數返回值爲5
funcs[5]() //=>5

這段代碼利用循環建立了不少閉包 ,當寫相似這種代碼的時候每每會犯一個錯誤:那就是試圖將循環代碼移入定義這個閉包的函數以內,看一下這段代碼:

 //返回一個函數組成的數組,它們的返回值是0-9
function constfuncs() {
    var funcs = [];
    for (var i = 0; i < 10; i++)
        funcs[i] = function() {
            return i;
        };
    return funcs;
}
var funcs = constfuncs();
console.log(funcs[5]()) //10

上面的這段代碼建立了10個閉包,並將它們存儲到一個數組中。這些閉包都是在同一個函數調用中定義的,所以它們能夠共享變量i。當constfuncs()返回時,變量i的值是10,全部的閉包都共享這一個值,所以,數組中的函數返回值都是同一個值,這不是咱們想要的結果。關聯到閉包的做用域鏈都是「活動的」,記住這一點很是重要。嵌套的函數不會將做用域內的私有成員負責一份,也不會對所綁定的變量生成靜態快照(static snapshot)。

書寫閉包的時候還須要注意一件事情,this是javascript的關鍵字,而不是變量。正如以前討論的,每一個函數調用都包含一個this值,若是閉包在外部的函數裏是沒法訪問this【嚴格將,閉包內的邏輯是可使用this的,但這個this和當初定義函數的this不是同一個,即使是同一個this,this的值是隨着調用棧的變化而變化的,而閉包裏的邏輯所取到的this的值也是不肯定的,所以外部函數內的閉包是可使用this的,但要很是當心的使用才行,做者在這裏提到的將this轉存爲一個變量的作法就能夠避免this的不肯定性帶來的歧義】,除非外部函數將this轉存爲一個變量:

var self = this; //將this保存到一個變量中,以便嵌套的函數可以訪問它

綁定arguments的問題與之相似。arguments並非一個關鍵字,但在調用每一個函數時都會自動聲明它,因爲閉包具備本身所綁定的arguments,所以閉包內沒法直接訪問外部函數的參數數組,除非外部函數將參數數組保存到另一個變量中:

var outerArguments = arguments; //保存起來以便嵌套的函數能使用它

在本章接下來的例子中就利用了這種編程技巧來定義閉包,以便在閉包中能夠訪問外部函數的this和arguments值。

7.函數屬性、方法和構造函數

咱們看到在javascript程序中,函數是值。對函數執行typeof運算會返回字符串「function」,可是函數是javascript特殊對象。由於函數也是對象,它們也能夠擁有屬性和方法,就像普通的對象能夠擁有屬性和方法同樣。甚至能夠用Function()構造函數來建立新的函數對象。

7.1.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; //再執行函數的後續邏輯
}

7.2.prototype屬性

每個函數都包含prototype屬性,這個屬性是指向一個對象的引用,這個對象稱爲原型對象(prototype object).每個函數都包含不一樣原型對象。當將函數用做構造函數的時候,新建立的對象會從原型對象上繼承屬性。

7.3.call()和apply()方法

能夠將call()和apply()看作是某個對象的方法,經過調用方法的形式來間接調用。

call()和apply()的第一個實參是要調用函數的母對象,它是調用上下文,在函數體內經過this來得到對它的引用。

f.call(o);
f.apply(o);

上面的例子每行代碼和下面代碼的功能類型(假設對象o中預先不存在名爲m的屬性):

o.m = f; //將f存儲爲o的臨時方法
o.m(); //調用它不傳入參數
delete o.m; //將臨時方法刪除

在ECMAScript5的嚴格模式中,call()和apply()的第一個實參都會變爲this的值,哪怕傳入的參數是原始值甚至是null或undefined。在ECMAScript3和非嚴格模式中,傳入的null和undefined都會被全局變量替代,而其它原始值會被相應的包裝對象(wrapper object)所替代。

調用上下文實參以後的全部實參就是要傳入待調用的函數的值。call與apply不一樣,apply的實參都放入一個數組中。

若是一個函數的實參能夠是任意數量,給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()函數接收兩個參數,一個對象和一個方法名,它將一個指定的方法替換爲一個新方法,這個新方法是「包裹」原始方法的令一個泛函數(泛函數也叫泛函,在這裏特指一個函數,以函數輸入,輸出的能夠是值也能夠是函數)。這種動態修改已有方法有時候叫作"monkey - patching".

7.4.bind()方法

bind()方法是ECMAScript5中新增的方法,可是ECMAScript3中能夠輕易模擬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)
console.log(g(4)) // => 5

也能夠經過如下代碼實現輕鬆綁定:

//返回一個函數,經過它來調用o中的方法f(),傳遞它全部的實參
function bind(f,o){    
    if(f.bind) return f.bind(o);//若是bind()方法存在的話,使用bind()方法
    else return function(){//不然這樣綁定
        return f.apply(o,arguments);
    }
}

ECMAScript5中的bind()方法不只僅是將函數綁定至一個對象,還附帶一些其它的應用:除了第一個實參以外,傳入bind()實參也會綁定至this,這個附帶的應用是一種常見的函數編程技術,有時也被稱爲「柯里化」(currying)。參照下面的這個例子中的bind()方法的實現:

var sum = function(x,y){return x + y};//返回練個個實參的值
//建立一個相似sum的新函數,但this的值綁定到null
//而且第一個參數綁定到1,這個新的參數指望只傳入一個實參
var succ = sum.bind(null,1);
succ(5)     // =>6 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的值並在ECMAScript3中實現這個附帶應用。例以下面的中的示例代碼就模擬實現了標準的bind()方法。

ECMAScript3的Function.bind()方法:

if(!Function.prototype.bind){
    Function.prototype.bind() = function(o /*,args*/){
        //將this和arguments的值保存至變量中
        //以便在後面的嵌套函數中可使用他們
        var self = this,boundArgs = arguments;
        
        //bind()返回值是一個函數
        return function(){
            //建立一個實參列表,將傳入bind()的第二個及後續的實參都傳入這個函數
            var arg = [],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變量,這兩個變量在閉包裏用到。儘管定義閉包的內部函數已經從外部函數中返回,並且調用這個閉包邏輯的時刻要在外部函數返回以後(在閉包中照樣能夠爭取訪問這兩個變量)。

ECMAScript5定義的bind()方法也有一些特性是上述ECMAScript3代碼沒法模擬的。首先,真正的的bind()方法返回一個函數對象,這個對象的length屬性是綁定函數的形參減去綁定實參的個數(length值不能小於0)。再者,ECMAScript5的bind()方法能夠順帶作構造函數,將忽略傳入bind()的this,原始函數就會以構造函數的形式調用,其實參也已經綁定(意思是在運行時將bind()所返回的函數用作構造函數時,所傳入的實參會原封不動的傳入原始函數)。由bind()方法返回的函數並不包含prototype屬性(普通函數的固有的prototype屬性是不能刪除的),而且將這些綁定的函數用作構造函數時鎖建立的對象從原始值的未綁定的構造函數中繼承prototype。一樣在使用instanceof運算符時,綁定構造函數和未綁定構造函數並沒有兩樣。

7.5.toString()方法

和全部的javascript對象同樣,函數也有toString()方法,ECMAScript規範規定這個方法返回一個字符串,這個字符串和函數聲明語句的語法相關。實際上,大多數(非所有)的toString()方法的實現都返回函數的完整源碼。內置函數每每返回一個"[native code]"的字符串做爲函數體。

7.6.Function()構造函數

函數還能夠經過Function()構造函數來定義:

var f = new Function("x","y","return x*y");

Function()構造函數能夠傳入任意數量的字符串實參,最後一個實參所表示的文本就是函數體;它能夠包含任意的javascript語句,每兩條語句之間用分號分隔。傳入構造函數的其餘全部的實參字符是指定函數的形參名字的字符串。若是定義的函數不包括任何參數,只須給構造函數簡單地傳入一個字符串--函數體--便可。

注意:Function()構造函數並不須要經過傳入實參以指定函數名。就像函數直擊量同樣,Function()構造函數建立一個匿名函數。

關於Function()構造函數有幾點須要注意:

  • Function()構造函數容許javascript在運行時動態的建立並編譯函數。
  • 每次Function()構造函數都會解析函數體,並建立新的函數對象。若是是在一個循環或者屢次調用的函數中執行這個構造函數,執行效率會受影響。相比之下 ,循環制的嵌套函數和函數定義表達式則不會每次執行時都從新編譯。
  • 最後一點,也是關於Function()構造函數很是重要的一點,就是它所建立的函數並非使用詞法的做用域。想法,函數體代碼的編譯老是會在頂層函數(也就是全局做用域)執行,正以下面代碼所示:
var scope = "global";

function constructFunction() {
        var scope = "local";
        return new Function("return scope"); //沒法捕捉局部做用域
    }
    //    這行代碼返回global,由於經過Function()構造函數所返回的戰術使用的不是局部做用域
constructFunction()(); //=>"global"

咱們能夠將Function()構造函數認爲是在全局做用域執行eval()(參照4.12.ii節),eval()能夠在本身的私有做用域內定義新變量和函數,Function()構造函數在實際編程過程當中不多用到。

7.7.可調用的對象

「類數組對象」並非真正的數組,但大部分場景下能夠將其當作數組來對待。對於函數也存在相似狀況。「可調用的對象」(callable object)是一個對象,能夠在函數調用表達式中調用這個對象。全部的函數都是可調用的,但非全部的可調用對象都是函數。

截止目前,可調用對象在兩個javascript實現中不能算做函數。首先,IE web瀏覽器(ie8及之前的版本)實現了客戶端方法(諸如window.alert()和document.getElementsById()),使用了可調用的宿主對象,而不是內置函數對象。IE的這個方法在其它瀏覽器中也都存在,但他們本質不是Function對象。IE9將它們實現爲真正的函數,所以這類可調用的對象愈來愈罕見。

另一個常見的可調用對象是RegExp對象(在衆多瀏覽器中均有實現),能夠直接調用RegExp對象,這比調用它的exec()方法更快捷一些。代碼最好不要對可調用的RegExp對象有太多依賴,這個特性在不久的未來可能會廢除並刪除。對RegExp執行typeof運算結果並不統一,有些瀏覽器中返回「function」,有些返回「object」。

若是想檢測一個對象是不是真值的函數對象(而且具備函數方法),能夠參照代碼檢測它的class屬性:

function isFunction(x) {
    return Object.prototype.toString.call(x) === "[object Function]"
}

8.函數式編程

和lisp、Haskell不一樣,javascript並不是函數式編程語言,但在javascript中能夠像操做對象同樣操控函數,也就是說能夠在javascript中應用函數式編程成績。ECMAScript5中的數組方法(諸如map()和reduce())就能夠很是適合用於函數式編程風格。

推薦你使用一下(至少閱讀一下)奧利弗·斯蒂爾(Oliver Steele)的函數式javascript庫

8.1.使用函數處理數組

假設有一個數組,數組的元素都是數字,咱們想要計算這些元素的平均值和標準差。若使用非函數式編程風格的話,代碼是這樣:

var data = [1, 1, 3, 5, 5, 6]; //這裏待處理的數組
 //平均數是全部元素的累加值和除以元素的個數
var total = 0;
for (var i = 0; i < data.length; i++) total += data[i]
 var mean = total / data.length; //=>3.5

 //計算標準差,首先計算每一個數減去平均數減去平均數以後誤差的平方而後求和
total = 0;
for (var i = 0; i < data.length; i++) {
    var deviation = data[i] - mean;
    total += deviation * deviation;
}
var stddev = Math.sqrt(total / (data.length - 1)); // 2.16794833886788 標準差的值

可使用數組方法,map()和reduce()來實現一樣的計算,這種實現極其簡潔:

//首先先簡單定義兩個簡單函數
var sum = function(x,y){return x+y;};
var square = function(x) {return x*x;};

//而後將這些函數和數組方法配合使用計算出平均數和標準差
var data = [1, 1, 3, 5, 5, 6]; //這裏待處理的數組
var mean =data.reduce(sum)/data.length;
var deviations = data.map(function(x){return x-mean;});
var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));

若是咱們基於ECMAScript3來如何實現呢?由於ECMAScript3並不包含這些數組方法,若是不存在內置方法咱們能夠自定義map()和reduce()函數:

。。。。。

8.2.高階函數

所謂高階函數(higer-order function)就是操做函數的函數,它接收一個或多個函數做爲參數,並返回一個新函數,看這個例子:

//這個高階函數返回一個新的函數,這個新函數將它的實參傳入f()
//並返回f的返回值邏輯非
function not(f){
 return function(){//返回一個新的函數
     var result = f.apply(this,arguments);//調用f()
     return !result; //對結果求反
 };
}
var even = function (x){//判斷a是否爲偶數的函數
 return x % 2 === 0;
};

var odd = not(even); //判斷一個新函數,和even()相反
[1,1,3,5,5].every(odd); //=>true 每一個元素爲奇數

上面的not()函數就是一個高階函數,由於它接收一個函數做爲參數,並返回一個新函數。另一個例子,來看下面的mapper()函數,它也是接收一個函數做爲參數,並返回一個新函數,這個新函數 將一個數組映射到另外一個使用這個函數的數組上。這個函數使用了以前定義的map()函數,但首先要理解這兩個函數有所不一樣的地方,理解這一點相當重要。

。。。。。。

8.3.不徹底函數

 

8.4.記憶

前面定義了一個階乘函數,它能夠將上次的計算結果緩存起來。在函數式編程當中,這種緩存技巧叫「記憶」(memorization)。

相關文章
相關標籤/搜索