函數是這樣一段代碼,它只定義一次,但可能被執行或調用任意次。你可能從諸如子例程(subroutine)或者過程(procedure)這些名字裏對函數概念有所瞭解。javascript
javascript函數是參數化的:函數定義會包括一個形參(parmeter)標識符列表。這些參數在函數中像局部變量同樣工做。函數會調用會給形參提供實參的值。函數使用它們實參的值計算返回值,成爲該函數的調用表達式的值。html
除了實參以外,麼次調用還會擁有一個值——本地調用的上下文——這就是this關鍵字值java
若是函數掛載在一個對象上,做爲對象的一個屬性,就稱爲它爲對象的方法。當經過這個對象來調用函數時,該對象就是這次調用的上下文(context),也就是該函數的this值。用於初始化一個新建立對象的函數稱爲構造函數(constructor).本文6節i會對構造函數進一步講解:第9章還會再談到它。node
在javascript中,函數即對象,程序可隨意操做它們。好比,javascript能夠把函數賦值給變量,或者做爲參數傳遞給其餘函數。由於函數就是對象,因此能夠給他們設置屬性,甚至調用它們的方法。程序員
javascript的函數能夠嵌套在其餘函數中定義,這樣他們就能夠訪問它們被定義時所處的做用域變量。這意味着javascript函數構成了一個閉包(closere),它給javascript帶來了很是強勁的編程能力。web
1.函數的定義。算法
函數使用function關鍵字來定義。它能夠用在函數定義表達式(4.iii)或者函數聲明語句裏。在這兩種形式中,函數定義都從function關鍵字開始,其後跟隨這些部分編程
下面的例子中分別展現了函數語句和表達式兩種方式的函數定義。注意:以表達式來定義函數只適用於它做爲一個大的表達式的一部分,好比在賦值和調用的過程當中定義函數。數組
//定義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))
注意:以表達式定義的函數,函數的名稱是可選的。一條函數聲明語句實際上聲明瞭一個變量。並把一個函數對象賦值給它。相對而言,定義函數表達式時並無聲明一個變量。函數能夠命名,就像上面的階乘函數,它須要一個名稱來指代本身。瀏覽器
若是一個函數定義表達式包含名稱,函數的局部變量做用域將會包含一個綁定到函數對象的名稱。實際上,函數的名稱將成爲函數內部的一個局部變量。一般而言,以表達式方式定義函數時不須要名稱,這會讓定義它們的代碼更緊湊。函數定義表達式特別適合用來那些只用到一次的函數,好比上面展現的最後兩個例子。
在5.3.ii中,函數聲明語句「被提早」到外部腳本或外部函數做用域的頂部,因此以這種方式聲明的函數,能夠被在它定義以前出現的代碼所調用。不過,以表達式定義的函數就令當別論了。
爲調用一個函數,必需要能引用它,而要使用一個表達式方式定義的函數以前,必須把它賦值給一個變量。變量的聲明提早了(參見3.10.i),但給變量賦值是不會提早的。因此,以表達式定義的函數在定義以前沒法調用。
請注意,上例中的大多數函數(但不是所有)包含一條return語句(5.6.iiii)。return語句致使函數中止執行。並返回它的表達式(若是有的話)的值給調用者。若是return語句沒有一個與之相關的表達式,則它返回undefined值。若是一個函數不包含return語句。那它就執行函數體內的每條語句,並返回undefined值給調用者。
上面例子中的函數大可能是用來計算出一個值的,他們使用return把值返回給調用者。而printprops()函數不一樣在於,它的任務是輸出對象各屬性的名稱和值。不必返回值,該函數不包含return語句,printprops()的返回值始終是undefined.(沒有返回值的函數有時候被稱爲過程)。
嵌套函數
在javascript中,函數能夠嵌套在其它函數裏。例如
function hyuse(a, b) { function square(x) { return x * x } return Math.sqrt(square(a) + square(b)); }
嵌套函數的有趣之處在於它的變量做用域規則:它們能夠訪問嵌套它們(或者多重嵌套)的函數的參數和變量。
例如上面的代碼裏,內部函數square()能夠讀寫外部函數hyuse()定義的參數a和b。這些做用域規則對內嵌函數很是重要。咱們會在本文第6節在深刻了解它們。
5.2.ii曾經說過,函數聲明語句並不是真正的語句。ECMAScript規範芝是容許它們做爲頂級語句。它們能夠出如今全局代碼裏,或者內嵌在其餘函數中,但它們不能出如今循環、條件判斷、或者try/cache/finally及with語句中(有些javascript併爲嚴格遵循這條規則,好比Firefox就容許在if語句中出現條件函數聲明)。注意:此限制僅適用於以語句形式定義的函數。函數定義表達式能夠出如今javascript的任何地方。
2.函數調用
構成函數主題的javascript代碼在定義之時並不會執行,只有調用該函數是,它們纔會執行。有4種方式來調用javascript函數。
i.函數調用
使用調用表達式能夠進行普通的函數調用也能夠進行方法調用(4.5)。一個調用表達式由多個函數表達式組成,每一個函數表達式都是由一個函數對象和左圓括號、參數列表和右圓括號組成,參數列表是由逗號分隔的逗號的零個或多個參數表達式組成。若是函數表達式是一個屬性訪問表達式,即該函數是一個對象的屬性或數組中的一個元素。那麼它就是一個方法調用表達式。下面展現了一些普通的函數調用表達式:
printprops({x: 1}); var total = distance(0,0,2,1) + distance(2,2,3,5); var probality = factorial(5)/factorial(13);
在一個調用中,每一個參數表達式(圓括號之間的部分)都會計算出一個值,計算的結果做爲參數傳遞給另一個函數。這些值做爲實參傳遞給聲明函數時定義的行參。在函數體中存在一個形參的調用,指向當前傳入的實參列表,經過它能夠得到參數的值。
對於普通的函數調用,函數的返回值成爲調用表達式的值。若是該函數返回是由於解釋器到達結尾,返回值就是undefined。若是函數返回是由於解釋器執行到一條return語句,返回的值就是return以後的表達式值,若是return語句沒有值,則返回undefined。
根據ECMAScript3和非嚴格的ECMAScript5對函數的調用規定,調用上下文(this的值)是全局對象。而後在嚴格模型下,調用上下文則是undefined、
以函數的形式調用的函數一般不使用this關鍵字。不過 ,「this」能夠用來判斷當前是否爲嚴格模式。
//定義並調用一個函數來肯定當前腳本運行是否爲嚴格模式 var strict = (function() {return !this;}())
ii.方法調用
一個方法無非是個保存在一個對象的屬性裏的javascript函數。若是有一個函數f和一個對象o,則能夠用下面的代碼給o定義一個名爲m()的方法:
o.m = f;
給o定義的方法m(),調用它時就像這樣:
o.m()
若是m()須要兩個實參,調用起來像這樣:
o.m(x,y)
上面的代碼是一個調用表達式:它包括一個函數表達式o.m,以及兩個實參表達式x和y,函數表達式的自己就是一個屬性訪問表達(4.4節),這意味着該函數被當作了一個方法,而不是做爲一個普通的函數來調用。
對方法調用的參數和返回值的處理,和上面所描述的普通函數調用徹底一致。可是方法調用和函數調用有一個重要的區別,即:調用上下文。屬性訪問表達式由兩部分組成:一個對象(本例中的o)和屬性名稱(m)。像這樣的方法在調用表達式裏,對象o成爲調用上下文,函數體可使用關鍵字this引用該對象。以下是具體的一個例子
var calcul = { //對象直接量 oprand1: 1, oprand2: 1, add: function() { //注意this關鍵字的用法,this指帶當前對象 return this.result = this.oprand1 + this.oprand2; } }; calcul.add(); //這個方法調用計算1+1的結果 calcul.result; //=>2
大多數方法調用使用點符號來訪問屬性,使用方括號(的屬性訪問表達式)也能夠進行屬性訪問操做。下面兩個例子都是函數的調用:
o["m"](x,y) //o.m(x,y)的另一種寫法 a[0](z)//一樣是一個方法調用(這裏假設a[0]是一個函數)
方法調用可能包含更復雜的函數屬性訪問表達式:
customer.surname.toUpperCase(); //調用customer.surname方法 f().m(); //在f()調用結束後繼續調用返回值中的方法m()
方法和this關鍵字是面向對象編程範例的核心。任何函數只要做爲方法調用實際上都會傳入一個隱式的實參——這個實參是一個對象,方法調用的母體就是這個對象。一般來說,基於那個對象的方法能夠執行多種操做,方法調用的語法已經很清晰地代表了函數將基於一個對象進行操做。比較下面兩行代碼:
rect.setSize(windth, height);
setrectSize(rect, width, heigth);
咱們假設這兩行代碼的功能徹底同樣,他們都做用域一個假定的對象rect。能夠看出,第一行的方法調用語法很是清晰地代表了這個函數執行的載體是rect對象,函數中的全部操做都將基於這個對象。
方法鏈 當方法的返回值是一個對象,這個對象還能夠再調用它的方法。這種方法調用序列中(一般稱爲「鏈」或者「級聯」)每次的調用結果都是另一個表達式組成部分。好比基於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();
須要注意的是,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
在8.7.iiii的例子中,有var self = this更切合實際的用法。
iii.構造函數的調用
若是函數或者方法以前帶有關鍵字new,它就構成構造函數調用(構造函數掉在4.6節和6.1.ii節有簡單介紹,第9章會對構造函數作更詳細的討論)。構造函數調用和普通的函數調用方法以及方法調用在實參處理、調用上下文和返回值各方面都不一樣。
若是構造函數調用圓括號內包含一組實參列表,先計算這些實參表達式,而後傳入函數內,這和函數調用和方法調用是一致的。但若是構造函數沒有形參,javascript構造函數調用的語法是容許省略形參列表和圓括號的。凡是沒有形參的構造函數均可以省略圓括號。以下文兩個代碼是等價的
var o = Object(); var o = Object;
構造函數調用建立一個新的 空對象,這個對象繼承自構造函數prototype屬性。構造函數試圖初始化這個新建立的對象,並將這個對象用作起調用上下文,所以構造函數能夠用this關鍵字來引用對象作起調用上下文,所以,構造函數可使用this關鍵字來引用這個新建立的對象。
注意:儘管構造函數看起來像一個方法調用,它依然會使用這個新對象做爲調用上下文。也就是說,在表達式new o.m()中,調用上下文並非o。
構造函數一般不使用return關鍵字,它們一般初始化新對象,當構造函數的函數體執行完畢時,它顯式返回。這種狀況下,構造函數調用表達式的計算結果就是這個新對象的值。然而,若是構造函數顯式的使用了return語句返回一個對象,那麼調用表達式的值就是這個對象。若是構造函數使用return語句但沒有指定返回值。或者返回一個原始值,那麼這時將忽略返回值。同時使用這個心對象做爲調用結果。
iiii.間接調用
javascript中的函數也是對象,和其它javascript對象沒有什麼兩樣,函數對象也能夠包含方法。其中的兩個方法call()和apply()能夠用來間接的調用函數。兩個方法都容許間接的調用函數。兩個方法都容許顯式指定調用所需的this值,也就是說,任何函數能夠做爲任何對象的方法來調用,哪怕這個函數不是那個對象的方法。兩個方法均可以指定調用的實參,apply()方法則要求以數組的形式傳入參數。8.7.iii會有關這兩種方法的詳細介紹。
3.函數的實參和形參
javascript中的函數定義並未指定函數的形參類型,函數調用也未對實參作任何類型的檢測。實際上javascript甚至不檢查傳入的形參的個數。下面幾節將會討論當調用函數時實參個數和聲明的形參個數不匹配時出現的情況。一樣說明了如何顯式測試函數實參的類型,避免非法的實參傳入函數。
i.可選形參
當調用函數的時候傳入的實參比函數聲明時指定的形參個數要少,剩下的的形參都將設置爲undefined值。所以,在調用函數的時,形參是否可選以及是否可選以及是否能夠省略應當保持 較好適應性。爲了作到這一點,應當給省略的參數賦一個合理的默認值、來看這個例子:
var xx = {x: 1,y: 2,z: 3}; var zz = [] //將對象o中的可枚舉屬性名追加到數組a中,並返回這個數組a //若是省略a,則建立一個新數組並返回這個新數組 function getPropertyNames(o, /*optional*/ a) { if (a === undefined) a = []; //若是a未定義,則使用新數組 for (var property in o) a.push(property); return a; } //這個函數調用時可使用兩個實參 getPropertyNames(xx); //將o的屬性存儲到一個新的數組中 getPropertyNames(xx, zz); //將p的屬性追加到數組a中
若是第一行代碼中不使用,可使用「||」運算符,若是第一個實參是真值的話就返回第一個實參;不然返回第二個實參。在這個場景下。若是做爲第二個實參傳入任意對象,那麼函數就會使用這個對象。若是省略掉第二個實參(或者傳遞null以及其餘任意假值),那麼就會建立一個新的空數組賦值給a。
(須要注意的是,使用「||」運算符代替if語句的前提是a必須先聲明,不然表達式會報引用錯誤,在這個例子中a是做爲形參傳入的,至關於var a,既然已經聲明a,因此這樣用是沒有問題的)
a = a || [];
回憶"||"運算符,若是第一個實參是真值的話就返回第一個實參;不然返回第二個實參。在這個場景下,若是做爲第二個實參傳入任意對象。那麼函數就會使用這個對象。
若是省略掉第二個實參(或者傳遞null或假值),那麼就會建立一個空數組,賦值給a。
須要注意的是,當用這種可選實參來實現函數時,須要將可選實參放在參數列表的最後。那行調用你的函數的人是沒辦法省略第一個實參傳入第二個實參的(它必須將undefined顯式傳入,【注意:函數的實參可選時每每傳入一個無心義的佔位符,慣用的作法是傳入null做爲佔位符,固然也可使用undefined】),一樣要注意在函數定義中,使用註釋/*optional*/來強調形參是可選的。
ii.可變長的實參列表:實參對象
當調用函數的時候,傳入的實參的個數大於函數定義的形參個數時,沒有辦法得到未命名值的引用。參數對象解決了這個問題。在函數體內,arguments是指向實參對象的引用,實參對象是一個類數組的對象(參照7章11節),這樣能夠經過數字下標就能訪問傳入函數的實參值。而不用非要經過名字來獲得實參。
假設定義函數f,它只有一個實參x。若是調用這個函數時須要傳入兩個實參,第一個實參能夠經過參數名x來得到,也能夠經過arguments[0]來獲得。第二個實參只能經過arguments[1]來獲得。此外和真正的數組同樣,arguments也包含一個length屬性,用以表示其所包含元素的個數。所以,調用函數f()時傳入兩個參數,arguments.length的值就是2.
實參對象在不少地方都很是有用,下面的例子展現了使用它來驗證明參的個數,從而調用正確的邏輯,由於javascript自己不會這樣作:
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,多出的實參會自動省略。
實參對象有一個重要的用處,就是讓函數操做任意數量的實參。下面的函數就能夠接受任意量的實參,並返回實參的最大值。(內置函數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; } max(1, 10, 222, 100); //=>222
相似這樣的函數能夠接收任意個實參,這種函數也叫「不定參函數」(varargs function),來自古老的c語言
注意:不定實參函數的實參個數不能爲零。arguments[]對象最適合的場景是在這樣一類函數中,這類函數包含固定個數的命名和必須參數,以及隨後個數不定的可選實參。
記住,arguments並非真正的數組。它是一個實參對象。能夠這樣理解:它是一個對象,碰巧有以數組索引的屬性。
數組對象包含一個非同尋常的特性。在非嚴格模式下,當一個函數包含若干形參,實參對象的數組元素是函數形參所對應實參別名,實參對象以數字索引,實參對象中以數字索引,而且形參名稱能夠能夠認爲是相同變量的不一樣命名。經過實參名字來修改實參值的話,經過arguments[]數組也能夠獲取到更改後的值,下面的這個例子清楚的說明了這一點。
function f(x) { console.log(x); //輸出實參的初始值 arguments[0] = null; //修改實參組的元素一樣會修改x的內容 console.log(x); //輸「null」 } f(11);
若是實參對象是一個普通的數組的話,第二條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); }
iii.將對象屬性用做實參
當一個函數包含超過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 });
iiii.實參類型
javascript方法的形參並未聲明類型,在傳入時也未作任何類型檢查。能夠在採用語義化的單詞來給函數命名,像上個例子中,給實參作補充註釋,以此使代碼文檔化。對於可選的實參來講,能夠在註釋中補充下「這個實參是可選的」。當一個方法能夠接收任意數量的實參時,可使用省略號。
function max( /* number*/ ) { /*代碼*/ }
3章8節提到,javascript會在必要的時候進行類型轉換。所以,函數指望接收一個字符串實參,而調用函數時傳入其它類型的值,所傳入的值會在函數體內將其用作字符串方法轉換爲字符串類型。全部原始類型均可以轉換爲字符串,全部對象都包含toString()方法(儘管不必定有用),因此這種 場景下不會有任何錯誤。
然而事情不老是這樣,上個例子中的arrayCopy()方法,這個方法指望他的第一個實參是一個數組,當傳入一個非數組的值做爲第一個實參時(一般會傳入數組對象),儘管看起來沒問題。但實際會出錯。除非所寫的函數是隻用到一兩次,用完即丟的那。你應當添加相似實參類型檢查邏輯,由於寧願程序在傳入非法值時報錯,也不肯意非法值致使程序報錯。
相比而言,邏輯執行時的報錯消息不甚清晰更難理解。下面的這個例子就作了這種類型檢測。本節借用7章11節isArrayLike()函數
//斷定o是不是一個類數組對象 //字符串和函數都length屬性,可是他們能夠有typeOf檢測將其排除 //在客戶端javascript中,DOM文本節點也有length屬性,須要用額外的o.nodetype != 3將其排除 function isArrayLike(o) { if (o && //o非null、undefined等 typeof o === "object" && //o是對象 isFinite(o.length) && //o.length是有限數 o.length >= o && //o.length是非負數 o.length === Math.floor(o.length) && //o.length是整數 o.length < 4294967296) //o.length < 2^32 return true; else return fasle; //不然它不是 } //返回數組(或類數組對象)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和undefiend if (isFinite(element)) total += element; else throw new Error("sum():elements must be a finte numbers"); } return total; } else throw new Error("sun():arguments mustbe array-like") }; a = [1,2,4,5,3,6,7]; sum(a)
這裏的sum()方法進行了很是嚴格的實參檢查,當傳入的非法的值的時候會拋出Error到控制檯。但當涉及類數組對象和真正的數組(不考慮數組元素是不是null仍是undefied),這種作法帶來的靈活性並不大。
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 nont convent" + element + "to number"); total += n; //不然,將n累加到total } return total; }
4.做爲值的函數
函數能夠定義,能夠調用,這是函數最重要的特性。函數定義和調用是javascript詞法特性,對於大多數編程語言來講也是如此。然而在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()方法能夠接受一個函數做爲參數,用來處理具體的排序操做。這個函數做用很是簡單,對於任意兩個值都返回一個值,以指定他們在爬行後的數組中的前後順序。這個函數參數使得Array.sort()具備更完美的通用性和無線擴展性,它能夠對任何類型的數據進行任意排序。7章8節iii有示例。
自定義函數屬性
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.做爲命名空間的函數
3章10節i介紹了函數做用域概念:
在 函數中聲明的變量在整個函數體內都是可見的(包括在嵌套的函數中),在函數的外部是不可見的。
不在任何函數內聲明的變量是全局變量,在整個javascript程序中都是可見的。
在javascript中是沒法聲明只在一個代碼塊內可見的變量的(在客戶端javascript中這種說法不徹底正確,在有些javascript擴展中就可使用let聲明語句塊內的變量,詳細內容見11章),基於這個緣由,咱們經常簡單定義一個函數用作臨時命名空間,在這個命名空間內定義的變量不會污染的全局命名空間。
好比,假設你寫了一段javascript模塊代碼,這段代碼將要用在不一樣的javascript程序中(對於客戶端javascript經常使用在各類網頁中)。和大多數代碼同樣,假定這段代碼定義了一個用以存儲中間計算結果的變量。
這樣,問題就來了,當模塊代碼放到不一樣的程序中運行時,你沒法得知這個變量是否已經建立了。若是已經存在這個變量,那麼將會和代碼發生衝突。
解決的辦法固然是將代碼放入一個函數內,而後調用這個函數。這樣全局變量就編程了函數內的局部變量:
function mymodule() { //模塊代碼 //這個模塊全部使用的全部變量是局部變量 //而不是污染全局命名空間 } mymodule(); //不要忘了還要調用的這個函數
這段代碼僅僅定義了一個單獨的全局變量,名叫「mymodule」的函數。這樣仍是太麻煩了,能夠直接定義一個匿名函數,並在單個表達式中調用它:
(function() { //mymodule函數重寫爲匿名函數表達式 //模塊代碼 }()); //結束函數定義並當即調用它
這種定義匿名函數並當即在單個表達式中調用它的寫法很是常見,已經成爲一種慣用的用法了。注意上面代碼的圓括號的用法,function以前的左括號是必須的,由於若是不寫這個左圓括號,javascript解釋器會試圖將其解析爲函數定義表達式。使用了它javascript解釋器纔會正確地將其解析爲函數定義表達式。使用圓括號是習慣用法,儘管有些時候沒有必要也不該當省略。這裏定義的函數會當即調用。
下面的例子展現了這種命名空間技術,它定義一個返回extend()函數的匿名函數,此外這個匿名函數命名空間用來隱藏一組屬性名。
/** * Created by lenovo on 2015/2/11. */ //在特定場景下返回帶補丁的extend()版本 //定義一個擴展函數,用來將第二個以及貴陽徐參數複製到第一個參數 //這裏咱們除了了IE bug:多ie版本中 //若是o屬性擁有一個不可枚舉的同名屬性,則for/in循環 //不會枚舉對象o的可枚舉屬性,也就是說 ,將不會掙錢的處理諸如toString的屬性 //除非咱們顯式的檢測它 var extend = (function() { //將這個函數的返回值賦給extend //在修復它以前,首先檢測是否存在bug for (var p in { toString: null }) { //若是代碼執行到這裏,那麼for/in循環會掙錢工做並返回 //一個簡單版本的extend()函數 return function extend(o) { for (var i = 1; i < arguments.length; i++) { var soure = arguments[i]; for (var prop in soure) o[prop] = soure[prop]; } return o; }; } //若是代碼執行到這裏,說明for/in循環 不會枚舉對象的toString屬性 //所以返回另一個版本的extend()函數,這個函數顯式測試 //Object.prototype中的不可枚舉屬性 return function patched_extend(o) { for (var i = 1; i < arguments.length; i++) { var soure = arguments[i]; //複製全部能夠枚舉的屬性 for (var prop in soure) o[prop] = soure[prop]; //如今檢查特特殊屬性 for (var j = 0; j < protoprops.length; j++) { prop = protoprops[j]; if (soure.hasOwnproperty(prop)) o[prop] = soure[prop]; } } return o; }; //這個列表列出看須要檢查的特殊屬性 var protoprops = ["toString", "valueOf", "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnummerable", "toLocaleString"]; } ());
6.閉包
和大多數現代編程語言同樣,javascript也採用詞法做用域(lexical scoping),也就是說,函數的執行依賴於變量做用域,這個做用域是在函數定義時決定的,而不是函數調用時決定的。
爲了實現這種詞法做用域,javascript函數對象的內部狀態不只包含函數的代碼邏輯,還必須引用當前的做用域(在於都本章節以前,應當複習下3.10節和3.10.iii講到的變量做用域和做用域鏈的概念)。
函數對象能夠經過做用域相互關聯起來,函數體內部的變量均可以保持在函數的做用域內,這種特性在計算機科學文獻中稱爲「閉包」。(這種叫法很是古老,是指函數的變量能夠隱藏於做用域鏈以內,所以看起來是函數將變量包裹了起來。)
從技術的做用域來說,全部的javascript函數都是閉包:它們都是對象,它們都關聯到做用域鏈。定義大多函數時的做用域鏈在調用函數時依然有效,但這不影響閉包。當調用函數時閉包所指向的做用域鏈不是同一個做用域鏈時,事情就變得很是微妙。
當一個函數嵌套了另一個函數,外部嵌套的函數對象做爲返回值返回的時候每每會發生這種事情。有不少強大的編程技術都利用到了這類嵌套的函數閉包,以致於這種編程模式在javascript中很是常見,當你第一次碰到很是讓人費解,一旦你理解和掌握閉包以後,就能很是的自如的使用它了。理解這一點相當重要。
理解閉包首先須要瞭解嵌套函數的詞法做用域規則,看一下這段代碼
var scope = "global scope"; //全局變量 function checkscope() { var scope = "local scope"; //局部變量 function f() { return console.log(scope); } //在做用域中返回這個值 return f(); } checkscope(); // local scope
checkscope()函數聲明瞭一個局部變量,並定於了一個函數f()返回了一個變量的值,最後將函數f()的執行結果返回便可,你應當很是清楚爲何checkscope()會返回「local scope」如今咱們將代碼改變下,你知道返回什麼嗎?
var scope = "global scope"; //全局變量 function checkscope() { var scope = "local scope"; //局部變量 function f() { return console.log(scope); } //在做用域中返回這個值 return f; } checkscope()(); //
在這段代碼中,咱們將函數內的一對圓括號移動到了checkscope()以後。checkscope()如今僅僅返回函數內嵌套的一個函數對象,而不是直接返回結果。在定義函數的做用域外面,調用這個嵌套的函數(包含最後一段代碼和最後一對圓括號)會發生什麼事情呢?
回想一下這個詞法做用域的基本規則:javascript函數的執行用到了做用域鏈。這個做用域鏈是函數定義的時候建立的。嵌套的函數f()定義在這個做用域鏈裏,其中的變量scope必定是局部變量,無論在什麼時候何地都執行函數f(),這種綁定在執行f()時依然有效。所以,最後一行返回"local scope",而不是「global」.簡言之,閉包的這個特性強大到讓人吃驚:它能夠捕捉到局部變量(和參數),並一直保存下來,看起來像這些變量綁定到在其中定義他們的外部函數。
實現閉包
若是你瞭解了詞法的做用域規則,你就能很容易地理解閉包:函數定義時的做用域鏈到函數執行時依然有效。然而不少程序員以爲閉包很是難理解,由於在深刻和興閉包的實現細節時將本身搞得暈頭轉向。他們以爲在外部函數中定義的局部變量在函數返回後就不存在了(之因此這麼說是由於不少人覺得函數執行結束後,與之相關的做用域鏈彷佛也不存在了,但在javascript中並不是如此),那麼嵌套的函數如何能調用不存在的做用域鏈呢?若是你想搞清楚這個問題,你須要更深刻的瞭解相似c語言這種更底層的編程語言,瞭解基於棧的cpu構架;若是一個函數的局部變量定義在cpu的棧中,那麼當函數返回時它們的確就不存在了。
但回想下3.10.iii節是如何定義做用域鏈的。咱們將做用域鏈描述爲一個對象列表,不是綁定的棧。每次調用javascript函數的時候,都會爲之建立一個新的對象用來保存局部變量,把這個對象添加至做用域鏈中。當函數返回的時候,就從做用域鏈中將這個綁定的變量的對象刪除。若是不存在嵌套的函數,也沒有其它引用指向這個綁定的對象,它就會被當作垃圾回收掉。若是定義了嵌套的函數,每一個嵌套的函數都各自對應一個做用域鏈,而且這個做用域鏈指向一個變量綁定對象。但若是這些嵌套的函數對象在外部函數中保留了下來,那麼它們也會和所指向的變量綁定對象同樣當作垃圾回收。可是若是這個函數定義了嵌套函數,並將它做爲返回值返回或者存儲在某處的屬性裏,這時就會有一個外部引用指向這個嵌套的 函數,它就不會被當作垃圾回收,而且它所指向的變量綁定也不會被當作垃圾回收(做者在這裏清楚地解釋了閉包和垃圾回收以前的關係,若是使用不慎,閉包很容易形成「循環引用」,當DOM對象和javascript對象以前存在循環引用時須要格外當心,在某些瀏覽器下會形成內存泄漏)。
本文4.i中定於了unInterger()函數,這個函數使用自身的一個屬性來保存每次返回的值,以便每次調用都能跟蹤上次的返回值。可是這種作法有一個問題,就是惡意代碼可能將計數器重置或者把一個非整數賦值給它,致使unInterger()函數不必定能產生「惟一」的「整數」。而閉包能夠捕捉到單個函數調用的局部變量,並將這些局部變量用作私有狀態,咱們能夠利用閉包重寫這個函數
var unInterger = (function() { //定義函數並當即調用 var counter = 0; //函數的私有狀態 return function() {return counter++;}; }());
要仔細閱讀這段代碼才能理解其含義,粗略來看,第一行代碼看起來像將函數賦值給一個變量unInterger,實際上,這段代碼定義了一個當即調用的函數(函數的開始帶有左圓括號),所以是這個函數的返回值賦給變量unInterger。如今咱們來看函數體,這是一個嵌套的函數,咱們將它賦值給變量unInterger,嵌套函數是能夠訪問做用域內的變量的,並且能夠訪問外部函數中定義的counter變量。當外部函數返回以後,其它任何代碼都沒法訪問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()函數返回了一個「計數器」對象,這個對象包含兩個方法:count()下返回一個整數,reset()將計數器重置爲內部狀態。
首先要理解,這兩個方法都能訪問私有變量n。再者,每次調用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()函數,這個函數定義了一個私有變量,以及兩個嵌套的函數來獲取和設置這個私有變量的值。它將這些嵌套函數添加爲所指定對象的方法。
利用閉包實現的私有屬性存取器的方法
利用閉包實現的私有屬性存取器的方法
//這個函數給對象o增長了屬性存取器方法 //方法名稱爲get<name>和set<name>.若是提供了一個斷定函數,setter方法就會用它來檢測參數的合法性,而後在存儲它。 //若是斷定函數返回false,setter方法拋出異常。 // //這個函數有一個非同尋常之處,就是getter和setter函數 //所操做的屬性值並無存儲在對象o中,相反,這個值僅僅是保存在函數中的局部變量中 //getter和setter方法一樣是局部函數,所以能夠訪問這個局部變量。也就是說,對於兩個存取器方法來講這個變量是私有的 //就沒有辦法繞過存取器方法來設置或修改這個值 function addPrivateProperty(o, name, predicate) { var value; //這是一個屬性值 //getter方法簡單地將其返回 o["get" + name] = function() {return value;}; //setter方法首先檢查值是否合法,若不合法就拋出異常,不然就將其存儲起來 o["set" + name] = function(v) { if (predicate && !predicate(v)) throw Error("set" + name + ":invalid value" + v); else value = v; }; } //下面展現了addPrivateProperty()方法 var o ={};//設置一個空對象 //增長屬性存取器方法getName()和setName() //確保只容許添加字符串值 addPrivateProperty(o,"Name",function(x){return typeof x == "string"; }); o.setName("Frank"); //設置屬性值 console.log(o.getName()); o.setName(o);//試圖設置一個錯誤類型的值
咱們已經給出了不少例子,在同一個做用域鏈中定義兩個閉包,這兩個閉包共享一樣的私有變量或變量。這是一種很是重要的技術,但仍是要當心那些不但願共享的變量每每不經意間共享給了其它的閉包,瞭解這一點很是重要。看一下下面的這段代碼:
//這個函數返回一個老是返回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()構造函數來建立新的函數對象。接下來的幾節就會着重介紹函數的屬性和方法,以及Function()構造函數。在第三部分也會有關於這些內容的講解。
i.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; //再執行函數的後續邏輯 }
ii.prototype屬性
每個函數都包含prototype屬性,這個屬性是指向一個對象的引用,這個對象稱爲原型對象(prototype object).每個函數都包含不一樣原型對象。當將函數用做構造函數的時候,新建立的對象會從原型對象上繼承屬性。6.1.3節討論了原型和prototype屬性,在第9章會有進一步討論。
iii.call()和apply()方法
咱們能夠將call()和apply()看作是某個對象的方法,經過調用方法的形式來間接調用(8.2.iiii)函數(好比在6.8.ii中使用call()方法來調用一個對象的Object.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; //將臨時方法刪除
在ECMAScript5的嚴格模式中,call()和apply()的第一個實參都會變爲this的值,哪怕傳入的參數是原始值甚至是null或undefined。在ECMAScript3和非嚴格模式中,傳入的null和undefined都會被全局變量替代,而其它原始值會被相應的包裝對象(wrapper object)所替代
對於call()來講,第一個調用上下文實參以後的全部實參就是要傳入待調用的函數的值。好比,以對象o的方法形式調用函數f(),並傳入兩個參數,可使用這樣的代碼。
f.call(o,1,2);
apply()方法和call()相似,但傳入的實參的形式和call()有所不一樣,它的實參都放入一個數組中:
f.apply(0, [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()函數接收兩個參數,一個對象和一個方法名,它將一個指定的方法替換爲一個新方法,這個新方法是「包裹」原始方法的令一個泛函數(反函數也叫泛函,在這裏特指一個函數)。這種動態修改已有方法有時候叫作"monkey - patching".
iiii.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()方法
注意,咱們將這個方法另存爲爲Function.prototype.bind,以便全部的函數對象都繼承它,這種技術會在9.4章節有詳細介紹。
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做爲哦的方法來調用,傳入這些實參 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運算符時,綁定構造函數和未綁定構造函數並沒有兩樣。
iiiii.toString
和全部的javascript對象同樣,函數也有toString()方法,ECMAScript規範規定這個方法返回一個字符串,這個字符串和函數聲明語句的語法相關。實際上,大多數(非所有)的toString()方法的實現都返回函數的完整源碼。內置函數每每返回一個"[native code]"的字符串做爲函數體。
iiiiii.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()構造函數很是重要的一點,就是它所建立的函數並非使用詞法的做用域。想法,函數體代碼的編譯老是會在頂層函數(也就是全局做用域)執行,正以下面代碼所示:
var scope = "global"; function constructFunction() { var scope = "local"; return new Function("return scope"); //沒法捕捉局部做用域 } // 這行代碼返回global,由於經過Function()構造函數所返回的戰術使用的不是局部做用域 constructFunction()(); //=>"global"
咱們能夠將Function()構造函數任務是在全局做用域執行eval()(參照4.12.ii節),eval()能夠在本身的私有做用域內定義新變量和函數,Function()構造函數在實際編程過程當中不多用到。
iiiiiii.可調用的對象
咱們在7.11節中提到「類數組對象」並非真正的數組,但大部分場景下能夠將其當作數組來對待。對於函數也存在相似狀況。「可調用的對象」(callable object)是一個對象,能夠在函數調用表達式中調用這個對象。全部的函數都是可調用的,但非全部的可調用對象都是函數。
截止目前,可調用對象在兩個javascript實現中不能算做函數。首先,IE web瀏覽器(ie8及之前的版本)實現了客戶端方法(諸如window.alert()和document.getElementsById()),使用了可調用的宿主對象,而不是內置函數對象。IE的這個方法在其它瀏覽器中也都存在,但他們本質不是Function對象。IE9將它們實現爲真正的函數,所以這類可調用的對象愈來愈罕見。
另一個常見的可調用對象是RegExp對象(在衆多瀏覽器中均有實現),能夠直接調用RegExp對象,這筆調用它的exec()方法更編輯一些。在javascript這是一個徹頭徹尾的非標準對象最開是由Netscape提出,後背其它瀏覽器廠商所複製,僅僅是爲了和Netscape兼容。代碼最好不要對可調用的RegExp對象有太多依賴,這個特性在不久的未來可能會廢除並刪除。對RegExp執行typeof運算結果並不統一,有些瀏覽器中返回「function」,有些返回「object」。
若是想檢測一個對象是不是真值的函數對象(而且具備函數方法),能夠參照代碼檢測它的class屬性(6章8節ii)
function isFunction(x) { return Object.prototype.toString.call(x) === "[object Function]" }
注意,這裏的isFunction()函數和7.10節的isArray()極其相似。
8.函數式編程
和lisp、Haskell不一樣,javascript並不是函數式編程語言,但在javascript中能夠像操做對象同樣操控函數,也就是說能夠在javascript中應用函數式編程成績。ECMAScript5中的數組方法(諸如map()和reduce())就能夠很是適合用於函數式編程風格。接下來的幾節將着重介紹javascript中的函數式編程技術。對javascript函數的探討會讓人倍感興奮,你會體會到javascript函數很是強大,而不只僅是學習一種編程風格而已(若是你對這部份內容感興趣,推薦你使用一下(至少閱讀一下)奧利弗·斯蒂爾(Oliver Steele)的函數式javascript庫)。
i.使用函數處理數組
假設有一個數組,數組的元素都是數字,咱們想要計算這些元素的平均值和標準差。若使用非函數式編程風格的話,代碼是這樣:
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()來實現一樣的計算,這種實現極其簡潔(參照7.9節來查看這些方法):
//首先先簡單定義兩個簡單函數 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()函數:
//對於每一個數組元素調用函數f(),並返回一個結果數組 //若是Array.prototype.map定義了的話,就使用這個方法 var map = Array.prototype.map ? function(a, f) { return a.map(f); } //若是已經存在map()方法,就直接使用它 : function(a, f) { //不然就本身實現一個 var result = []; for (var i = 0, len = a.length; i < len; i++) { if (i in a) result[i] = f.call(null, a[i], i, a); return result; } }; //使用函數f()和可選的初始值將數組a減至一個值 //若是Array.prototype.reduce存在的話,就使用這個方法 var reduce = Array.prototype.reduce ? function(a, f, initial) { //若是reduce()方法存在的話 if (arguments.length > 2) return a.reduce(f, initial); //若是成功的傳入了一個值 else return a.reduce(f); //不然沒有初始值 } :function(a,f,initial){//這個算法來自ECMAScript5規範 var i =0,len =a.length,accumulator; //以特定的初始值開始,不然第一個值取自a if(arguments.length>2) accumulator = initial; else {//找到數組中第一個已經定義的索引 if(len == 0) throw TypeError(); while(i<len){ if(i in a){ accumulator = a[i++]; break; }else i++; }if(i == len) throw TypeError(); } //對於數組中剩下的元素一次調用f() while(i<len){ if(i in a) accumulator = f.call(undefined,accumulator,a[i],i,a); } return accumulator; };
使用定義的map()和reduce()函數,計算平均值和標準差的代碼看起來像這樣:
var data = [1,2,35,6,3,2]; var sum =function(x,y){return x+y;}; var square = function(x){return x*x;}; var mean =reduce(data,sum)/data.length; var deviations = map(data,function(x){return x-mean;}); var stddev = Math.sqrt(reduce(map(deviations,square),sum)/(data.length-1));
ii.高階函數
所謂高階函數(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()函數,但首先要理解這兩個函數有所不一樣的地方,理解這一點相當重要。
var map = Array.prototype.map ? function(a, f) { return a.map(f); } //若是已經存在map()方法,就直接使用它 : function(a, f) { //不然就本身實現一個 var result = []; for (var i = 0, len = a.length; i < len; i++) { if (i in a) result[i] = f.call(null, a[i], i, a); return result; } }; // 所返回的函數的參數應當是一個實參數組,並對每一個函數數組元素執行函數f() // 並返回全部的計算結果組成數組 // 能夠對比下這個函數和上下文提到的map()函數 function mapper(f) { return function(a) { return map(a, f); }; } var increment = function(x) {return x + 1;}; var incrementer = mapper(increment); incrementer([1, 2, 3]) // => [2,3,4]
這裏是一個更常見的例子,它接收兩個函數f()和g(),並返回一個新的函數用以計算f(g()):
//返回一個新的可計算f(g(...))的函數 //返回的函數h()將它全部的實參傳入g(),而後將g()的返回值傳入f() //調用f()和g()時的this值和調用h()時的this值是同一個this function compose(f,g){ return function(){ //須要給f()傳入一個參數,因此使用f()的call方法 //須要給g()傳入不少參數,因此使用g()的apply()方法 return f.call(this,g.apply(this,arguments)); }; } var square = function(x){return x*x;}; var sum = function(x,y){return x+y;}; var squareofsum = compose(square,sum); squareofsum(2,10) //=>144
本章後幾節中定義了partial()和memozie函數,這兩個函數都是很是重要的高階函數。
iii.不徹底函數
函數f()(見8.7.iiii)的bind()方法返回一個新函數,而後給新函數傳入特意的上下文和一組指定的參數,讓調用函數f()。咱們說它把函數「綁定至」對象並傳入一個部分參數。bind()方法只是將實參放在(完整參數列表的)左側,也就是說傳入的bind()的實參都是放在傳入原始函數的實參列表開始的位置。但有時咱們指望傳入bind()實參放在(完整實參列表)右側:
// 實現一個工具函數將類數組對象(或對象)轉換爲正真的數組 // 在後面示例代碼中用到了這個方法將arguments對象轉化爲正真的數組 function array(a, n) {return Array.prototype.slice.call(a, n || 0);} //這個函數的實參傳遞至左側 function partialLeft(f /*,...*/ ) { var args = arguments; //保存外部實參數組 return function() { //並返回這個函數 var a = array(args, 1); //開始處理外部的地圖份額args a = a.concat(array(arguments)); //而後增長內全部內部實參 return f.apply(this, a); //而後基於這個實參列表調用f() }; } //這個函數的實參傳遞至右側 function partialRight(f /*,...*/ ) { var args = arguments; //保存外部實參數組 return function() { //返回這個函數 var a = array(arguments); //從內部參數開始 a = a.concat(array(args, 1)); //而後從外部第一個args開始添加 return f.apply(this, a); //而後基於這個實參列表調用f() }; } //這個函數的實參被用作模板 //實參列表中的undefeined值都被填充 function partial(f /*,...*/ ) { var args = arguments; //保存外部實參數組 return function() { var a = array(args, 1); //從外部的args開始 var i = 0, j = 0; //遍歷args,從內部實參填充undefined值 for (; i < a.length; i++) if (a[i] === undefined) a[i] = arguments[j++]; //如今將剩下的內部實參都追加進去 a = a.concat(array(arguments, j)) return f.apply(this, a); }; } //這個函數帶有三個實參 var f = function(x, y, z) { return x * (y - z); }; //注意三個不徹底調用以前的區別 partialLeft(f, 2)(3, 4) //=>-2: 綁定第一個實參:2*(3-4) partialRight(f, 2)(3, 4) //=>6: 綁定最後一個實參:3*(4-2) partial(f, undefined, 2)(3, 4) //=>-6 綁定中間的實參:3*(2-4)
利用這種不徹底函數的編程技巧,能夠編寫一些有意思的代碼,利用已有的函數定義新的函數。參照下, 這個例子
var increment = partialLeft(sum,1); var cuberoot = partialRight(Math.pow,1/3); String.prototype.first = partial(String.prototype.charAt,0); String.prototype.last = partial(String.prototype.substr,-1,1);
當不徹底調用和其餘高階函數整合在一塊兒的時候,事情就變得格外有趣了。好比這個理例子定義了not()函數,它用到了剛纔提到不徹底調用:
var not = partialLeft(compose,function(x){return !x;}); var even = function(x) {return x % 2 === 0;}; var odd = not(even); var isNumber = not(isNaN)
咱們也可使用不徹底調用的組合來從新足足求平均數和標準差的代碼,這種編碼風格是很是純粹的函數式編程:
var data = [1,1,3,5,5] var sum =function(x,y){return x+y;}; //兩個初等函數 var product =function(x,y){return x*y;}; var neg = partial(product-1); var square = partial(Math.pow,undefined,2); var sqrt = partial(Math.pow,undefined,.5); var reciprocal = partial(Math.pow,undefined,-1);
咱們也可使用不徹底調用的組合來從新足足求平均數和標準差的代碼,這種編碼風格是很是純粹的函數式編程:
//如今來計算平均值和標準差,全部的函數調用都不帶運算符 //這段代碼看起來很像lisp代碼 var mean = product(reduce(data,sum),reciprocal(data.length)); var stddev = sqrt(product(reduce(map(data, compose(square, partial(sum,neg(mean)))) ,sum), reciprocal(sum(data.length,-1)))); console.log(mean)
iiii.記憶
在8.4.i中定義了一個階乘函數,它能夠將上次的計算結果緩存起來。在函數式編程當中,這種緩存技巧叫「記憶」(memorization)。下面代碼展現了一個高階函數,memorize()接受一個函數做爲實參,並返回帶有以及能力的函數。(須要注意的是,記憶只是一種編程技巧,本質上是犧牲算法的空間複雜度以換取更優的事件複雜度,在客戶端javascript中的代碼的執行時間複雜度每每成爲瓶頸,所以在大多數場景下,這種犧牲空間換取事件的作法以提高程序執行效率的作法是很是可取的。)
function memorize(f) { var cache = {}; //將值保存在閉包內 return function() { //將實參轉換爲字符串形式,並將其用作緩存的鍵 var key = arguments.length + Array.prototype.join.call(arguments, ","); if (key in cache) return cache[key]; else return cache[key] = f.apply(this, arguments); }; }
memorize()函數建立一個新的對象,這個對象被當作緩存(的宿主)並賦值給一個局部變量,所以對於返回的函數來講它是私有的(在閉包中)。所返回的函數將它的實參轉換爲字符串,並將字符串用作緩存對象的屬性名。若是在緩存中存在這個值,則直接返回它。
不然,就調用既定的函數對實參進行計算,將結果緩存起來並返回,下面的代碼展現瞭如何使用memorize():
//返回兩個整數的最大公約數 //使用歐吉利德算法 function gcd(a,b){//這裏省略對a和b的類型檢查 var t; if (a>b) t=b,b=a,a=t; //確保a>=b while(b !=0) t=b, b= a%b, a=t; //這裏是求最大公約數的歐幾里德算法 return a; } var gcdmemo = memorize(gcd); gcdmemo(85,187); //=>17 //注意,咱們寫一個遞歸函數時,每每須要實際記憶功能 //咱們更但願調用了實現了記憶功能的遞歸函數,而不是原遞歸函數 var factorial = memorize(function(n){ return(n <= 1)?1:n *factorial(n-1); }); factorial(5) //=>120 對4-1的值也有緩存
(本文完結,臨近春節,祝你們新年快樂。歡迎你們關注第9章內容:javascript類和模塊)