[toc]
## 函數定義
函數使用function關鍵字來定義,可以用在函數定義表達式或者函數聲明語句中。
> 需要注意的是,用function聲明的函數會被提前,但是通過表達式的方式定義的函數只是變量提前了,賦值操作沒提前,因此不能在表達式定義之前調用這個函數
### 嵌套函數
```javascript
function hypotenuse(a, b){
function square(x){
return x*x
}
return Math.sqrt(square(a) + square(b));
}
```
嵌套函數的有趣之處在意變量作用域規則:它們可以訪問嵌套它們(或者多重嵌套)的函數的參數和變量。
## 函數調用
- 作為函數
- 作為方法
- 作為構造函數
- 通過apply()方法和call()方法調用
### 函數調用
作為普通的函數調用, 函數的返回值成為調用表達式的值。如果該函數返回是因為解釋器到達結尾,返回值就是undefined。如果有return 語句,則返回return的值。
以函數形式調用的函數通常不使用this關鍵字。不過,“this”可以用來判斷當時是不是嚴格模式
```javascript
var isStrict = (function(){ return !this }());
```
### 方法調用
> 方法是保存在對象屬性中的js函數
方法調用中的this指向的是當前對象。
```javascript
var calculator = {
operand1: 1,
operand2: 2,
add: function(){
this.result = this.operand1 + this.operand2
},
child: {
operand1: 3,
operand1: 4,
add: function(){
//此處的this只的是child這個上下文
this.result = this.operand1 + this.operand2
}
}
};
calculator.add();
calculator.child.add();
calculator.result //--> 2
```
**方法和this關鍵字是面向對象編程的核心**,任何函數只要作為方法調用實際上都會傳入一個隱式的實參--這個實參就是一個對象,方法調用的母體就是這個對象。通常來講,基于那個對象的方法可以執行多種操作,方法調用的語法很清晰的表明函數基于對象進行操作。
```javascript
rect.setSize(width. height);
setRectSize(rect, width, height);
```
假設這兩行代碼的功能完全一樣,都作用于一個假定的對象rect。第一行的方法調用語法非常清晰的表明這個函數執行的載體是rect對象,函數中所有操作都是基于這個對象的。
> 方法鏈----當方法的返回值是一個對象,這個對象還可以再調用它的方法。這種方法調用序列中每次的調用結果都是另外一個表達式的組成部分。
> 當方法并不需要返回值時,最好直接返回this。如果在設計的API中一直采用這種方法(每個方法都返回this), 使用API就可以進行“鏈式調用”風格的編程了。例如: shape.setX(100).setY(100).setOutline('red').draw();
> 不要將方法的鏈式調用和構造函數的鏈式調用混為一談
this是一個關鍵字,不是變量也不是屬性名,不能給this賦值。
this沒有作用域的限制,嵌套函數不會從調用它的函數中繼承this。如果嵌套函數作為方法調用,其this值指向調用它的對象。如果嵌套函數作為函數調用,this指向全局對象在嚴格模式下是undefined。
調用嵌套函數時,如果需要訪問外部函數的this值,需要將this賦值給一個變量。
```javascript
var o = {
m: function(){
console.log(this === o);
var self = this;
function f(){
console.log(this === o);
console.log(self === o);
};
f();
}
}
```
### 構造函數
> 如果函數或者方法調用之前帶有關鍵字new, 它就構成構造函數。構造函數調用和普通的函數調用以及方法調用在實參處理、調用上下文和返回值方面都有不同。
如果構造函數調用在圓括號內的一組實參列表,先計算這些實參表達式,然后傳入函數內,這和函數調用方法調用是一致的。
凡是沒有形參的構造函數調用都可以省略括號。
構造函數調用創建一個新的空對象,這個對象繼承自構造函數的prototype屬性。構造函數試圖初始化這個新創建的對象,并將這個對象用做其調用上下文,因此構造函數可以使用this關鍵字來引用這個新創的對象。
注意,盡管構造函數看起來像一個方法調用,它依然會使用這個新對象作為調用上下文,在表達式new o.m()中,調用的上下文是new的新對象而不是o
構造函數通常不使用return關鍵字,它們通常初始化新對象,當構造函數的函數體執行完畢時,它會顯式返回。在這種情況下,構造函數調用表達式的計算結果就是這個新對象的值。
如果構造函數顯式的使用return語句返回一個對象,那么調用表達式的值就是這個對象。如果構造函數使用return語句但是沒有執行返回值,或者返回一個原始值,那么它將忽略返回值,同時使用這個新對象作為調用結果。
### 間接調用
> 函數對象也有兩個方法, call和apply,可以間接的調用函數。兩個方法都允許顯式指定調用所需的this值,任何函數可以作為任何對象的方法來調用,即使這個函數不是那個對象的方法。兩個方法都可以制定法調用的參數。call()方法使用它自有的實參列表作為函數的實參,apply()則要求以數組的形式傳入參數。
## 函數的實參和形參
> js 中的函數定義并未指定函數形參的類型,函數調用也沒有對傳入的實參做任何類型檢查。 函數調用實際上都不檢查傳入形參的個數
### 可選形參
> 當調用函數的時候傳入的參數比函數聲明時指定的形參個數少,那么剩下的形參都將設置為undefined值。因此在調用函數時心肝是否可選以及是否可以省略應當保持較好的適應性。可以給省略的參數設置一個默認值
```javascript
function getPropertyNames(o, a){
a = a || [];
for (var prop in o) a.push(prop);
return a;
}
```
### 可變長的實參列表:實參對象
> 為了解決傳入的實參個數超過形參個數導致無法獲取未命名的值時,標識符arguments通過指向實參對象的引用,解決了這個問題。
實參對象有一個重要的用處,可以讓函數操作任意數量的實參。(Math.max()的功能與之類似)
```javascript
function max(/******/){
var max = Number.NEGATIVE_INFINITY;
for (var i = 0; i < arguments.length; i++){
arguments[i] > max ? max = arguments[i] : max = max;
}
return max;
}
var largest = max(1,10,100,1111); //--> 1111
```
上面這種函數也被稱為“不定實參函數”,這個術語源自古老的C語言。
arguments并不是真正的數組,它是一個實參對象。每個實參對象都是以數字為所以呢的一組元素以及length屬性。
在非嚴模式下,當一個函數包含若干形參,實參對象的數組元素是函數形參對用實參的別名,實參對象中以數字索引,并且形參名稱可以認為是相同變量的不同命名。通過實參名字來修改實參值得花,通過arguments[]數組也可以獲取到更改后的值。
```javascript
function f(x){
console.log(x);
arguments[0] = null;
console.log(x);
}
```
**在es5中移除了實參對象的這個特殊特性。在嚴格模式下,arguments變成一個保留字,無法使用arguments作為形參名或者局部變量名,不能給arguments賦值**
**callee和caller屬性**
除了數組元素,實參對象還定義了callee和caller屬性。**在嚴格模式下,對這兩個屬性的讀寫操作都會產生類型錯誤**。在非嚴格下,callee屬性指代當前正在執行的函數。**caller是非標準的**,但大多數瀏覽器都實現了這個屬性,它指代調用當前正在執行的函數的函數。通過caller屬性可以訪問調用棧。callee屬性在某些時候很有用,比如在匿名函數中通過callee來遞歸調用自身。
```javascript
var factorial = function(x){
if(x <= 1) return 1;
return x*arguments.callee(x-1);
}
```
### 將對象屬性用作實參
當一個函數包含超過三個形參時,建議以對象形式傳入,還不用考慮實參的順序。
### 實參類型
js方法的形參并沒有聲明類型,可以在拋出異常前盡可能的轉換類型或者停止代碼運行。
```javascript
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()); //如果是函數就調用ta并作類型轉換
else
n = Number(element); //否則直接強轉類型
if(isNaN(n)){
throw Error(element + 'is not a number')
}
total += n;
}
return total;
}
```
## 作為值的函數
> 函數不僅是一種語法,也是值。可以將函數賦值給變量,存儲在對象的屬性或數組的元素中,作為參數傳入另一個函數等
```javascript
function square(x){ return x*x }
```
這個定義創建了一個新的函數對象,并將其賦值給變量square。函數的名字實際上是看不見的,square僅僅是變量的名字,這個變量指代函數對象。函數還可以賦值給其他的變量,并且扔可以正常工作。
```javascript
var s = square;
square(4) === s(4);
```
## 自定義函數屬性
js中的函數并不是原始值,而是一種特殊的對象,函數其實是可以擁有屬性的。例如:
```javascript
uniqueInteger.counter = 0;
function uniqueInteger(){
return uniqueInteger.counter++;
}
```
下面的函數使用了自身的屬性來緩存上一次的計算結果:
```javascript
function factorial(n){
if(isFinite(n) && n > 0 && Math.round(n)){ //有限的正整數
if(!(n in factorial)){ //如果沒有緩存結果
facrotial[n] = n * factorial(n - 1);//計算并緩存
}
return factorial[n]; //返回緩存結果
}else{
return NaN; //如果輸入有誤,則返回NaN
}
}
fatorial[1] = 1; //初始化緩存以保存這種基本情況
```
## 作為命名空間的函數
> 在js中我們無法聲明只在一個代碼塊內可見的變量,因此我們常常定義一個函數用作臨時的命名空間,在這個命名空間內定義的變量不會污染到全局命名空間
```javascript
//定義一個擴展函數,用來將第二個以及后續的參數復制到第一個參數
//這里處理的IE的bug:在多數ie中如果o屬性擁有一個不可枚舉的同名屬性,則for/in循環不會枚舉對象o的可枚舉屬性
//將不會正確的處理toString的屬性
//除非我們顯式檢測它
var extend = (function(){
for (var p in {toString: null}){
return function extend(o){
for(var i = 1; i < arguments.length; i++){
var source = arguments[i];
for(var prop in source){o[prop] = source[prop]};
}
return o;
};
}
return function patched_extend(o){
for(var i = 1; i < arguments.length; i++){
var source = arguments[i];
for(var prop in source) o[prop] = source[prop];
for(var j = 0; j < protoprops.length; j++){
prop = protoprops[j];
if(source.hasOwnProperty(prop)) o[prop] = source[prop];
}
}
return o
};
var protoprops = ['toString', 'valueOf', 'constructor', 'hasOwnproperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString']
}())
```
## 閉包
和其他變成語言一樣,函數的執行依賴于變量作用域,**這個作用域在函數定義時決定的**。為了實現這種詞法作用域,javascript函數對象的內部狀態不僅包含函數的代碼邏輯,還必須引用當前的作用域鏈。函數對象可以通過作用域鏈相互關聯,函數體內部的變量都可以保存在函數作用域內,這種特性成為**“閉包”**。
從技術角度上說,所有的js函數都是閉包: **它們都是對象,都關聯到作用域鏈。** 丁輝大多數函數時的作用域鏈在調用函數時依然有效,單著不影響閉包。當調用函數時閉包所指向的作用域鏈和定義函數時的作用域鏈不是同一個作用域鏈時,事情就變得非常微妙。當一個函數嵌套了另一個函數,外部函數將嵌套的函數對象作為返回的時候往往會法傷這種事情。有很多強大的編程技術都利用了這類嵌套的函數閉包,以至于這種編程模式在JavaScript中非常常見。當第一次碰到閉包時會非常讓人費解。
理解閉包首先要了解嵌套函數的詞法作用域規則。看下面的代碼。。
```javascript
var scope = 'global scope';
function checkscope(){
var scope = 'local scope';
function f(){
return scope;
}
return f();
}
checkscoe(); //-->'local scope'
```
checkscope()函數聲明了一個局部變量,并定義了一個函數f(),函數f()返回了這個變量的值,最后將函數f()的執行結果返回。
```javascript
var scope = 'global scope';
function checkscope(){
var scope = 'local scope';
function f(){
return scope;
}
return f;
}
checkscoe()(); //-->'local scope'
```
返回的還是local scope!!!回想下詞法作用域的基本原則:JavaScript函數的執行到了作用域鏈,這個作用域鏈式函數定義的時候創建的。嵌套函數的f()定義在這個作用域里,其中的變量scope一定是局部變量,不管在何時何地執行f(),這種綁定在執行f()時依然有效。因此在最后一行代碼返回“local scope”。簡言之,**閉包的這個特性可以捕捉到局部變量(和參數),并一直保存下來**,看上去像這些變量綁定到了再其中定義它們的外部函數。
在8.4.1中定義了uniqueInteger()函數,這個函數用自身屬性來保存每次的值,這可能會被人為的清0,而閉包不會。可以用閉包重寫這個函數。
```javascript
var uniqueInteger = (function(){
var count = 0;
return function(){
return count++;
};
}());
uniqueInteger(); //0
uniqueInteger(); //1
uniqueInteger(); //2
```
這段代碼定義了一個立即調用的函數,返回值賦值給變量uniqueInteger。函數體內返回了另一個函數,這個一個嵌套函數,我們將它賦值給變量定義的count。當外部函數返回之后,其他代碼都無法訪問count變量,只有內部函數才能訪問它。
**像count一樣的私有變量不是只能在一個單獨的比包內,在同一個外部函數內定義的多個嵌套函數都能訪問它,這多個嵌套函數都共享一個作用域鏈。**
```javascript
function counter(){
var n = 0;
return {
count: function(){
return n++;
},
reset: function(){
n = 0
}
}
}
var c = counter(),
d = counter();
c.count(); //0
d.count(); //0 兩個互不干擾
c.reset(); //0 reset()和count()共享狀態
d.count(); //1
a.count(); //0
```
counter()函數返回了一個“計數器”對象,對象的兩個方法屬性都可以訪問私有變量n。**每次調用counter()都會創建一個新的作用域鏈和一個新的私有變量**。因此調用counter()兩個,則會得到兩個計數器對象,而且彼此包含不同的四右邊哦啊寧。
從技術角度看,其實可以將這個閉包合并為屬性存取器方法getter和setter。
```javascript
function counter(n){
typeof Number(n) === 'number' ? n = Number(n) : n = 0
return {
get count(){return n++;}
//setter不允許n遞減
set count(m){
if( m >= n) n = m;
else throw Error('can only be set to l larger value')
}
}
}
```
需要注意的是這個版本的counter()函數并未聲明局部變量,只是用參數n來保存私有狀態,屬性存取器方法可以訪問n,這樣的話counter()的函數就能指定私有變量的初始值了。
例8-4是這種使用閉包技術來共享私有狀態的通用做法。下面的例子定義了一個私有變量,以及兩個嵌套函數來獲取和設置這個變量的值。它將這些嵌套函數添加為指定對象的方法:
例8-4:利用閉包實現的私有屬性存取器方法
```javascript
//這個函數給對象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()
}else{
value = v
}
}
}
var o = {};
addPrivateProperty(o, "Name", function(x){return typeof x == 'string'});
o.setName('James');
console.log(a.getName());
```
在同一個作用域鏈中定義兩個閉包,這兩個閉包共享同樣的私有變量或變量。但是要小心那些不希望共享的變量往往共享給了其他閉包。
```javascript
function constfunc(v){return function(){return v}}
var funcs = [];
for(var i = 0; i < 10; i++){
funcs[i] = constfunc(i);
}
funcs[5]() //-->5
```
這段代碼利用循環創建了很多歌閉包,當寫這種類似的代碼是會翻一個錯誤:試圖將循環代碼移入定義這個閉包的函數之內:
```javascript
//返回一個函數組成的數組,它們的返回值是0-9
function constfuncs(){
var funcs = [];
for(var i = 0; i < 10; i++){
funcs[i] = function(){ return i };
}
return funcs
}
var funcs = consfuncs();
funcs[5]() //10
```
上面這段代碼創建了10個閉包,但是這些閉包都是在同一個函數調用中定義的,因此它們共享變量i。當constfuncs()返回時,變量i的值是10,所有的閉包都共享這一個值,因此,數組中的函數的返回值都是同一個值。
、關聯到閉包的作用域鏈都是“活動的”,嵌套的函數不會講作用域內的私有成員復制一份,也不會對所綁定的變量生成靜態快照。
書寫閉包時還需注意的是,this是js的關鍵字,而不是變量。正如之前討論的,每個函數調用都包含一個this值,如果閉包在外部函數里是無法訪問this的,除非在外部函數將this轉存為一個變量:
```javascript
var self = this;
```
綁定arguments的問題與之類似。arguments并不是一個關鍵字,倒在調用每個函數時都會自動聲明它,由于閉包具有自己所綁定的arguments,因此閉包內飾無法直接訪問外部函數的arguments的,除非像this一樣處理:
```javascript
var outerArguments = arguments;
```
## 函數屬性、方法和構造函數
在js程序中,函數是值。對函數執行typeof運算會返回字符串"function",但函數是js重特殊的對象。也可以擁有屬性和方法。甚至可以用Function()構造函數來創建新的函數對象。
### length屬性
在函數體里,arguments.length表示傳入函數的實參的個數。而函數本身的length屬性則有這不同含義。函數的length是只讀屬性,它代表函數形參的數量。這是的參數指的是“形參”而非實參,也就是函數定義時給的參數個數。
```javascript
//這個函數使用arguments.callee,因此不能在嚴格模式下工作
function check(args){
var actual = args.length, //實參的真實個數
expected = args.callee.length //期望的實參個數
if(actual !== expected){
throw Error('...') //如果不同則拋出異常
}
}
function f(x, y, z){
check(arguments); //檢查實參個數和期望的實參個數是否一致
return x + y + z; //再執行后續邏輯
}
```
### prototype屬性
每一個函數都包含一個prototype屬性,這個屬性是指向一個對象的引用,這個對象稱為“原型對象”。每一個函數都包含不同的原型對像。當將函數用作構造函數的時候,新創建的對象會從原型上繼承屬性。6.1.3節討論了原型和prototype屬性。
### call()方法和apply方法()
call()和apply()的第一個實參是要調用函數的木對象,它是調用上下文在函數體內通過this來獲得對它的引用。
```javascript
f.call(o);
f.apply(o)
```
在ECMAscript5的嚴格模式中,call()和apply()的第一個實參都會變成this的值,哪怕傳入的實參是原始值甚至是null或undefined。在非嚴格模式中,傳入null和undefined會被全局對象代替。而其他原始值則會被相應的包裝對象所帶一。
call()和apply()的不同之處在于傳入的實參,call是按順序傳入實參,而apply則以數組形式傳入實參
需要注意的是,傳入apply()的參數數組可以是類數組對象也可以是真是數組,實際上,可以將當前函數的arguments數組直接傳入apply()來調用另一個函數:
### bind()方法
bind()方法是ECMAscript5中新增的方法。這個方法主要作用是將函數綁定到某個對象。
```javascript
//這是待綁定的函數
function f(y){ return this.x + y }
var o = { x: 1 }, //將要綁定的對象
g = f.bind(o); //通過調用g(x)來調用o.f(x)
g(2) //-->3
```
還可以通過以下代碼實現這種綁定:
```javascript
function bind(f, o){
if(f.bind) return f.bind();
else return function(){
return f.apply(o, arguments);
}
}
```
zaiECMAScript5中的bind()方法不僅僅是將函數綁定至一個對象,它還附帶一些其他應用:除了第一個實參以外,傳入bind()的實參也會綁定至this,這個附帶的應用是一種常見的函數式編程技術,有時也被成為“柯里化”。
```javascript
var sum = function(x, y){
return x + y //返回兩個參數的和值
},
succ = sum.bind(null, 1);
succ(2) //-->3: x綁定到1,并傳入2作為實參y
function f(y, z){
return this.x + y + z; //另外一個做累加計算的函數
}
var g = f.bind({x: 1}, 2);
g(3) //-->6: this.x綁定到1,y綁定2,z綁定到3
```
我們可以綁定this的值并在es3中實現這個附帶的應用。例8-5中模擬實現了標準的bind()方法
注意,我們將這個方法另存為Function.prototype.bind,以便所有的函數對象都繼承他。
例8-5:es3版本的bind()方法實現
```javascript
if(!Function.prototype.bind){
Function.prototype.bind = function(o/* ,args*/){
//將this和arguments的值保存至變量中
//以便在后面的嵌套函數中可以使用
var self = this,
boundArgs = arguments;
//bing()方法的返回值是一個函數
return function(){
//創建一個實參列表,將傳入bind()的第二個及后續的實參都傳入這個函數
var args = [],
i;
for(i = 1; i < boundArgs.length; i++){
args.push(boundArgs[i]);
};
for(i = 0; i < arguments.length; i++){
args.push(arguments[i])
};
//現在將self作為o的方法來調用,傳入這些實參
return self.apply(o, args);
}
}
}
```
bind()方法返回的函數是一個閉包,這個閉包的外部函數中聲明了self和boundArgs變量,這兩個變量在閉包里用到。盡管定義閉包的內部函數已經從外部函數中返回,而且調用這個閉包邏輯的時刻要在外部函數返回之后(在閉包中照樣可以正確訪問這兩個變量)。
ECMAScript5定義的bind()方法也有一些特性是es3代碼無法模擬的。
bing()方法返回的是一個函數對象,這個函數對象的length屬性是綁定函數的形參個數減去綁定實參的個數(length的屬性不能小于零)。
再者,es5的bind()可以順帶用構造函數。如果bind()返回的函數用作構造函數,將忽略傳入bind()的this,原始函數就會以構造函數的形式調用,其實參也已經綁定。有bind()方法所返回的函數并不包含prototype屬性(普通函數固有的prototype屬性是不能刪除的),并且將這些綁定的函數用作構造函數所創建的對象從原始的未綁定的構造函數中繼承prototype。
同樣,在使用instanceof運算符時,綁定構造函數和未綁定構造函數并無兩樣。
### toString()方法
函數也有toString()方法,實際上,大多數的toString()方法的實現都是返回函數的完整源碼,內置函數則返回"[ native code ]"的字符串作為函數體
### Function()構造函數
不管是用過函數定義語句還是函數直接量表達書,函數的定義都要使用function關鍵字。單數函數還可以通過Function()構造函數來定義:
```javascript
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()構造函數都會解析函數體,并創建新的函數對象。如果是一個循環或者多次調用的函數中執行這個構造函數,執行效率會受到影響。相比之下,循環=中的潛逃函數和函數定義表達式不會每次執行時都重新編譯。
- 最后一點,它所創建的函數并不是使用詞法作用域,相反,**函數體代碼的編譯總是會在頂層函數執行**:
```javascript
var scope = "global";
function constructFunction(){
var scope = "local";
return new Function('return scope'); //無法捕獲局部作用于
}
constructFunction()(); //-->'global'
```
我們可以將Function()構造函數認為是在全局作用域中執行的eval(),Function()構造函數在實際編程過程中很少用。
### 可調用的對象
在7.11節中提到的“類數組對象”并不是真正的數組,但大部分場景下,可以將其當做數組來對待。“可調用的對象”是一個對象,可以在函數調用表達式中調用這個對象。所有的函數都是可調用的,但并非所有可調用對象都是函數。
可調用對象在兩個js實現中不能算作函數。首先,IE Web瀏覽器(ie8之前的版本)實現了客戶端方法(如document.getElementById()),使用了可調用的宿主對象,而不是內置函數對象。IE中的這些方法在其他瀏覽器中也存在,但它們本質上不是Function對象。IE9將它們實現為真正的函數,因此這類可調用的對象將越來越罕見。
另外一個常見的可調用對象是RegExp,可以直接調用RegExp對象。這比調用它的exec()方法更快一些。在js中這是一個非標準特性。
對RegExp執行typeof,有的返回'function'有的返回'object'
下面的函數可以檢測一個對象是否是真正的函數對象:
```javascript
function isFunction(x){
return Object.prototype.toString.call(x) === '[object function]'
}
```
## 函數式編程
和Lisp、Haskell不同,JavaScript并非函數式編程語言,但是在js中可以像操控對象一樣操控函數。
es5中的數組方法(如map和reduce)就適用于函數式編程。
### 使用函數處理數組
假設有一個數組,元素都是數字。用非函數式編程的風格來和函數式編程的風格來計算平均值和標準差:
```javascript
var data = [1, 1, 3, 5, 5],
total = 0;
for(var i = 0; i < data.length; i++){
total += data[i]
}
var mean = total/data.length;
//計算標準差
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));
```
用數組方法map()和reduce()同樣可以實現:
```javascript
var sum = function(x, y){ return x + y },
square = function(x){ return x * x },
data = [1, 1, 3, 5, 5],
mean = data.reduce(sum)/data.length,
deviations = data.map(function(x){ return x - mean }),
stddv = Math.sqrt(deviations.map(square),reduce(sum)/(data.length))
```
### 高階函數
高階函數就是操作函數的函數,它接收一個或多個函數作為參數,并返回一個新的函數:
```javascript
function not(f){
return function(){ //返回一個新函數
var result = f.apply(this, arguments); //調用f()
return !result; //對結果求反
}
}
var even = function(x){
return x%2 === 0; //判斷x是否是偶數
}
var odd = not(even); //一個新的函數,所做的事情和even相反
[1,3,5,7].every(odd); //true每個都是奇數
```
not()函數就是一個高階函數,它接收一個函數作為參數,并返回一個新函數。看下一個mapper()函數:
```javascript
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]
```
還有個更常見的例子:
```javascript
//返回一個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 },
sum = function(x, y){ return x + y },
squareofsum = compose(square, sum);
squareofsum(2, 3); //25
```
### 不完全函數
函數f()的bind()方法返回一個新函數, 給新函數傳入特定的上下文和一組指定的參數,然后調用函數f()。我們說它把函數“綁定至”對象并傳入一部分參數。傳入bind()的參數都是放在傳入原始函數的實參列表開始的地方。
我們可以將傳入bind()的實參放在右側:
```javascript
//實現一個工具函數將數組對象(或對象)轉換成真正的數組
//將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);
a = a.concat(array(arguments));
return f.apply(this, a)
}
}
//這個函數的實參傳遞至右側
function partialRight(f /*, ...*/){
var args = arguments;
return function(){
var a = array(arguments);
a = a.cancat(array(args, 1));
return f.apply(this, a);
}
}
//這個函數的實參被用做模板
//實參列表中的undefined值都被填充
function partial(f /*, ...*/){
var args = arguments;
return function(){
var a = array(args, 1)
var i = 0,
j = 0;
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
partialRight(f, 2)(3, 4); //6
partial(f, undefined, 2)(3, 4); //-6
```