函數是這樣一段代碼,它隻定義一次,但可能被執行或調用任意次。你可能從諸如子例程(subroutine)或者過程(procedure)這些名字裡對函數概念有所了解。
javascript函數是參數化的:函數定義會包括一個形參(parmeter)辨別符清單。這些參數在函數中像局部變量一樣工作。函數會調用會給形參提供實參的值。函數使用它們實參的值計算傳回值,成為該函數的調用表達式的值。
除了實參之外,麼次調用還會擁有一個值——本地調用的上下文——這就是this關鍵字值
如果函數挂載在一個對象上,作為對象的一個屬性,就稱為它為對象的方法。當通過這個對象來調用函數時,該對象就是此次調用的上下文(context),也就是該函數的this值。用于初始化一個新建立對象的函數稱為構造函數(constructor).本文6節i會對構造函數進一步講解:第9章還會再談到它。
在javascript中,函數即對象,程式可随意操作它們。比如,javascript可以把函數指派給變量,或者作為參數傳遞給其他函數。因為函數就是對象,是以可以給他們設定屬性,甚至調用它們的方法。
javascript的函數可以嵌套在其他函數中定義,這樣他們就可以通路它們被定義時所處的作用域變量。這意味着javascript函數構成了一個閉包(closere),它給javascript帶來了非常強勁的程式設計能力。
1.函數的定義。
函數使用function關鍵字來定義。它可以用在函數定義表達式(4.iii)或者函數聲明語句裡。在這兩種形式中,函數定義都從function關鍵字開始,其後跟随這些部分
- 函數名稱辨別符:函數明川是函數聲明語句必須的部分。它的用途就像是變量的名字,新定義的函數對象會指派給這個變量。對函數定義表達式來說,這個名字是可選的:如果存在,該名字隻存在函數中,并代指函數對象本身。
- 一對圓括号:其中包含由0個或者多個逗号隔開的辨別符組成的清單。這些辨別符是函數的參數明川,它們就像函數體中的局部變量一樣。
- 一對花括号,裡邊包含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】),同樣要注意在函數定義中,使用注釋來強調形參是可選的。
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類和子產品)
轉載于:https://www.cnblogs.com/ahthw/p/4282745.html