第八章:Javascript函數


函數是這樣一段代碼,它只定義一次,但可能被執行或調用任意次。你可能從諸如子例程(subroutine)或者過程(procedure)這些名字里對函數概念有所了解。

javascript函數是參數化的:函數定義會包括一個形參(parmeter)標識符列表。這些參數在函數中像局部變量一樣工作。函數會調用會給形參提供實參的值。函數使用它們實參的值計算返回值,成為該函數的調用表達式的值。

除了實參之外,么次調用還會擁有一個值——本地調用的上下文——這就是this關鍵字值

如果函數掛載在一個對象上,作為對象的一個屬性,就稱為它為對象的方法。當通過這個對象來調用函數時,該對象就是此次調用的上下文(context),也就是該函數的this值。用於初始化一個新創建對象的函數稱為構造函數(constructor).本文6節i會對構造函數進一步講解:第9章還會再談到它。

在javascript中,函數即對象,程序可隨意操作它們。比如,javascript可以把函數賦值給變量,或者作為參數傳遞給其他函數。因為函數就是對象,所以可以給他們設置屬性,甚至調用它們的方法。

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

1.函數的定義。

函數使用function關鍵字來定義。它可以用在函數定義表達式(4.iii)或者函數聲明語句里。在這兩種形式中,函數定義都從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))

注意:以表達式定義的函數,函數的名稱是可選的。一條函數聲明語句實際上聲明了一個變量。並把一個函數對象賦值給它。相對而言,定義函數表達式時並沒有聲明一個變量。函數可以命名,就像上面的階乘函數,它需要一個名稱來指代自己。

如果一個函數定義表達式包含名稱,函數的局部變量作用域將會包含一個綁定到函數對象的名稱。實際上,函數的名稱將成為函數內部的一個局部變量。通常而言,以表達式方式定義函數時不需要名稱,這會讓定義它們的代碼更緊湊。函數定義表達式特別適合用來那些只用到一次的函數,比如上面展示的最后兩個例子。

在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函數。

  • 作為函數
  • 作為方法
  • 作為構造函數
  • 通過它們的call()或apply()方法間接調用

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類和模塊


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM