[TOC]
# 第15章 函數
函數是可以調用的值。定義函數的一種方法稱為函數聲明。例如,以下代碼定義了具有單個參數`x`的函數`id`:
```js
function id(x) {
return x;
}
```
`id`函數中,`return`語句返回一個值,您可以通過它的名稱來調用函數,后面是括號中的參數
```js
> id('hello')
'hello'
```
如果不從函數返回任何東西,返回`undefined`(隱式地):
```js
> function f(){}
> f()
undefined
```
這部分只顯示了一種定義函數和調用函數的一種方式。其他的將在后面描述。
## 函數在JavaScript中的三個角色
一旦您定義了一個函數,它就可以扮演多個角色:
### 非方法函數(“正常函數”)
您可以直接調用函數。然后它作為一個正常的函數。這是一個示例調用:
```js
id('hello')
```
按照慣例,正常函數的名稱以小寫字母開始。
### 構造函數
您可以通過new操作員調用函數。然后它成為一個構造函數,一個對象的工廠。這是一個示例調用:
```js
new Date()
```
按照慣例,構造函數的名稱以大寫字母開頭。
### 方法
您可以將函數存儲在對象的屬性中,將其轉換為可以通過該對象調用的方法。 這是一個調用示例:
```
obj.method()
```
按照慣例,方法名稱以小寫字母開頭。
本章介紹非方法功能; [第17章](###)解釋了構造函數和方法。
## 術語:“Parameter”對比“Argument”
> 簡略描述為:parameter=形參(formal parameter), argument=實參(actual parameter)。
術語 Parameter 和 Argument 都經常互換使用,因為上下文通常會讓你明白意圖的含義是什么。以下是區分它們的經驗法則。
* *Parameter* 用于定義一個函數。它們也被稱為formal parameters (形式參數)和 formal arguments (形參)。在以下示例中,param1并且param2是形參:
```js
function foo(param1, param2) {
...
}
```
* *Arguments* 用于調用函數。它們也稱為 actual parameters (實際參數)和 actual arguments (實參)。在以下示例中,3并且7是實參:
```js
foo(3, 7);
```
## 定義函數
本節介紹創建函數的三種方法:
1. 通過函數表達式
2. 通過函數聲明
3. 通過構造函數 `Function()`
所有函數都是對象,都是`Function`的實例:
```js
function id(x) {
return x;
}
console.log(id instanceof Function); // true
```
因此,函數從`Function.prototype`中獲取它們的方法。
### 函數表達式
函數表達式產生一個值 - 一個函數對象。例如:
~~~
var add = function (x, y) { return x + y };
console.log(add(2, 3)); // 5
~~~
該代碼將函數表達式的結果分配給變量add,并通過該變量調用它。函數表達式產生的值可以分配給一個變量(如最后一個例子所示),作為參數傳遞給另一個函數等等。因為正常的函數表達式沒有名稱,它們也被稱為匿名函數表達式。
#### 命名函數表達式
命名函數表達式(Named Function Expression,即“有名字函數表達式”,與“匿名函數”相對。——譯者注)
你可以給一個函數表達式賦值一個名字。命名函數表達式允許**函數表達式引用自身,這對自我遞歸有用**:
```js
var fac = function me(n) {
if (n > 0) {
return n * me(n-1);
} else {
return 1;
}
};
console.log(fac(3)); // 6
```
<table>
<th style="text-align:center">注意</th>
<tr>
<td>
命名函數表達式的名稱只能在函數表達式中訪問:
```js
var repeat = function me(n, str) {
return n > 0 ? str + me(n-1, str) : '';
};
console.log(repeat(3, 'Yeah')); // YeahYeahYeah
console.log(me); // ReferenceError: me is not defined
```
</td>
</tr>
</table>
### 函數聲明
以下是一個函數聲明:
```js
function add(x, y) {
return x + y;
}
```
前面看起來像一個函數表達式,但它是一個語句(請參見[表達式與語句](###第7章))。它大致相當于以下代碼:
```js
var add = function (x, y) {
return x + y;
};
```
換句話說,函數聲明聲明一個新變量,創建一個**函數對象(即:new Function)**,并將其分配給該變量。
### 函數構造器
構造函數 `Function()`對存儲在字符串中的JavaScript代碼進行解析運行,例如,下面的代碼與前面的示例是一樣的:
```js
var add = new Function('x', 'y', 'return x + y');
```
但是,這種定義函數的方法是運行緩慢的,并且將代碼保存在字符串中(工具無法訪問)。因此,如果我們盡可能還是使用函數表達式或函數聲明。[解析運行代碼](##第23章)一章更詳細地解釋了`Function()`,它和`eval()`類似。
## 提升(Hoisting)
提升意味著“**移動到作用域的開始位置**”。
函數聲明被完全地提升,只有部分變量聲明被提升。
功能聲明完全懸掛。這允許您在聲明之前調用函數:
```js
foo();
function foo() { //該函數被提升
...
}
```
上述代碼的工作原理是JavaScript引擎將`foo`函數聲明移動到范圍的開頭。上面的代碼,就會像下面這樣被執行:
```js
function foo() {
...
}
foo();
```
`var`的聲明部分會被提升,賦值部分不會。因此,使用var聲明和與前一個示例類似的函數表達式會導致錯誤:
```js
foo(); // TypeError: undefined is not a function
var foo = function () {
...
};
```
只有變量聲明被提起。JS引擎會這樣執行上面的代碼:
```js
var foo;
foo(); // TypeError: undefined is not a function
foo = function () {
...
};
```
## 函數的名稱
大多數JavaScript引擎都支持函數對象的非標準屬性`name`。函數聲明有:
```js
> function f1() {}
> f1.name
'f1'
```
匿名函數表達式的名稱是空字符串:
```js
> var f2 = function(){};
> f2.name
“”
```
但命名函數表達式有一個名稱::
```js
> var f3 = function myName() {};
> f3.name
'myName'
```
函數的名稱對于調試非常有用。有些人出于這個原因會給出函數表達式的名字。
## 哪個更好:函數聲明或函數表達式?
如果您更喜歡像下面這樣的函數聲明?
```js
function id(x) {
return x;
}
```
還是一個`var`聲明加上一個函數表達式達到同等效果?
```js
var id = function (x) {
return x;
};
```
它們基本相同,但函數聲明相對于函數表達式有兩個優點:
1. 它們被提升(參見[提升](##)),因此在它們出現在源代碼之前,您可以調用它們
2. 它們有一個名稱(參見[函數的名稱](###))。然而,JavaScript引擎在推斷匿名函數表達式的名稱方面做得越來越好。
## 更多控制函數的調用:call()、apply()和bind()
`call()`,`apply()`和`bind()`是所有函數具有的方法(記住函數是對象,因此具有方法)。它們可以在調用方法時為`this`提供一個值,因此在面向對象的上下文中主要是有趣的(請參閱[調用函數:call()、apply()和bind()](###第17章#oop_call_apply_bind))。
本節解釋了非對象方法的兩個用例。
### func.apply(thisValue,argArray)
此方法調用函數時使用 `argArray`的元素作為參數; 也就是說,以下兩個表達式是等效的:
```js
func(arg1, arg2, arg3)
func.apply(null, [arg1, arg2, arg3])
```
執行func時`thisValue`的值被傳遞給函數內的`this`。在非面向對象中不需要設置它,因此在它被設置為`null`。
當一個函數以類似數組的方式(但不是數組)接受多個參數時,可以使用`apply()`
多虧`apply()`,我們可以使用`Math.max()`(參見[其他函數](###第21章#Math_max))來確定數組的最大元素:
```js
> Math.max(17, 33, 2)
33
> Math.max.apply(null, [17, 33, 2])
33
```
### func.bind(thisValue, arg1, ..., argN)
它執行時是偏函數用法 - 創建了一個新的函數,該函數將調用func,并將其`this`設置為`thisValue`并且下面的參數是:`arg1`,到`argN`,是新函數的實際參數。`hisValue`在以下非面向對象中不需要設置,所以被它設置為`null`。
在這里,我們使用`bind()`來創建一個新的函數`plus1()`,它就像`add()`,但是它只需要參數`y`,因為`x`始終是1:
```js
function add(x, y) {
return x + y;
}
var plus1 = add.bind(null, 1);
console.log(plus1(5)); // 6
```
換句話說,我們創建了一個相當于以下代碼的新函數:
```js
function plus1(y) {
return add(1, y);
}
```
## 處理缺失或額外參數
JavaScript有強制一個函數的參數數量:你可以用任意數量的實際參數來調用它,而與定義的形式參數無關。因此,實際參數和形式參數的數量可以有兩種不同:
**實際參數比形式參數多**
多出的參數被忽略,但可以通過特殊的類數組變量`arguments`(稍后將討論)來獲取。
**實際參數比形式參數少**
缺少的形式參數的值為`undefined`。
### 所有參數可以按索引訪問:特殊的變量參數
特殊變量`arguments`存在函數內部(包括方法)。它是一個類似數組的對象,它保存當前函數調用的所有實際參數。以下代碼使用它:
```js
function logArgs() {
for (var i=0; i<arguments.length; i++) {
console.log(i+'. '+arguments[i]);
}
}
```
這是交互結果輸出::
```
> logArgs('hello', 'world')
0. hello
1. world
```
`arguments` 具有以下特點:
* 它是類數組的,但不是數組。一方面,它有一個`length`屬性,并且可以通過索引讀取和寫入各個參數。
另一方面,arguments不是一個數組,它只是類似的。它沒有數組方法(slice(),forEach(),等等)。幸運的是,您可以借用數組方法或將`arguments`轉換為數組,如[Array-Like Objects和Generic Methods](##第17章#array-like_objects)所述。
* 它是一個對象,因此所有對象方法和操作符都是可用的。例如,您可以使用`in`運算符(迭代和檢測屬性)來檢查`arguments`是否具有給定的索引:
```js
> function f() { return 1 in arguments }
> f('a')
false
> f('a', 'b')
true
```
您可以以類似的方式使用`hasOwnProperty()`(迭代和屬性檢測):
```js
> function g() { return arguments.hasOwnProperty(1) }
> g('a', 'b')
true
```
#### 棄用的參數的特性
嚴格模式會降低一些`arguments`的不尋常的特性:
* `arguments.callee` 指向了當前函數。它主要用于匿名函數中的自遞歸,并且在嚴格模式下不允許。作為解決方法,使用命名函數表達式(請參閱[命名函數表達式](###)),它可以通過其名稱引用自身。
* 在非嚴格模式下,如果你改變了一個參數,`arguments`也會保持最新:
```js
function sloppyFunc(param) {
param = 'changed';
return arguments[0];
}
console.log(sloppyFunc('value')); // 被改變的
```
在嚴格的模式下,這種保持更新的特性會失效:
```js
function strictFunc(param) {
'use strict';
param = 'changed';
return arguments[0];
}
console.log(strictFunc('value')); // value
```
* 嚴格模式禁止給`arguments`賦值(例如,通過`arguments++`)。仍然允許分配給其他元素和屬性。
### 強制性參數,強制執行最低的的參數數量
有三種方法來判斷一個參數是否丟失。
首先,你可以檢查它是否為`undefined`:
```js
function foo(mandatory, optional) {
if (mandatory === undefined) {
throw new Error('Missing parameter: mandatory');
}
}
```
其次,您可以將該參數解釋為一個布爾值。那么`undefined`就被認為是`false`的。但是,還有一個警告:其他幾個值也被認為是假的(參見[Truthy和Falsy值](###)),所以這個檢查不能區分。
打個比方:`0`和一個缺失的參數,都為`false`:
```js
if (!mandatory) {
throw new Error('Missing parameter: mandatory');
}
```
第三,你也可以檢查的`arguments`的長度,以強制執行最低的參數數量:
```js
if (arguments.length < 1) {
throw new Error('You need to provide at least 1 argument');
}
```
最后一個方法與其他方法不同:
* 前兩種方法不區分`foo()`和`foo(undefined)`。在這兩種情況下,拋出異常。
* 第三種方法`foo()`會拋出一個異常,并為`foo(undefined)`設置可選的選項。
### 可選參數
如果一個參數是可選的,這意味著如果一個參數缺失,則給它一個默認值。 與強制參數類似,有四種選擇。
首先,檢查`undefined`:
```js
function bar(arg1, arg2, optional) {
if (optional === undefined) {
optional = 'default value';
}
}
```
第二,將`optional`解釋為布爾型::
```js
if (!optional) {
optional = 'default value';
}
```
第三,您可以使用`||`操作符(參見[Logical Or(||)](###第10章#logical_or)),如果它為`true`,則返回左邊的操作數。否則,它返回右邊的操作數:
```js
// Or operator: use left operand if it isn't falsy
optional = optional || 'default value';
```
第四,您可以通過`arguments.length`的方式檢查函數的參數數量:
```js
if (arguments.length < 3) {
optional = 'default value';
}
```
最后一個方法與其他方法不同:
* 前三種方法不區分`bar(1, 2)`和`bar(1, 2, undefined)`。在這兩種情況下`optional`都是`'default value'`。
* 第四種方法設置`bar(1, 2)`下的`optional`為'default value',`bar(1, 2, undefined)`下的`optional`值為`undefined`。
另一種可能是將可選參數作為命名參數,作為對象字面量的屬性(請參閱[命名參數](http://speakingjs.com/es5/ch15.html#named_parameters))。
### 模擬引用傳遞參數
在JavaScript中,不能通過引用傳遞參數; 也就是說,如果將一個變量傳遞給一個函數,它的值將被復制并傳遞給函數(通過值傳遞)。因此,這個函數不能更改這個變量。如果需要這樣做,則必須將變量的值封裝起來(例如:一個數組)。
此示例演示了增加變量的函數:
```js
function incRef(numberRef) {
numberRef[0]++;
}
var n = [7];
incRef(n);
console.log(n[0]); // 8
```
### 缺陷:意想不到的可選參數
> 方法簽名由**方法名稱**和一個**參數列表**(方法的參數的順序和類型)組成。
方法的簽名可以唯一的確定這個函數。
如果你把一個函數`c`作為一個參數傳遞給另一個函數`f`,那么你必須知道兩個簽名:
* 函數`f`需要知道其參數的簽名,`f`可以提供幾個參數,讓`c`來決定使用幾個參數(如果有的話)。
* 函數`c`的實際簽名,例如,`c`可能支持可選參數。
如果兩者有差異,你就會得到意想不到的結果:c原本可能有你不知道的可選參數并且這將解釋函數`f`提供的附加參數不正確。
例如,數組方法`map()`的參數是只有具有單個參數的普通函數(參見[轉換方法](###d第18章)):
```js
> [ 1, 2, 3 ].map(function (x) { return x * x })
[ 1, 4, 9 ]
```
可以作為參數傳遞的一個函數是`parseInt()`(參見[通過parseInt()得到的整數](###第11章)):
```js
> parseInt('1024')
1024
```
你可能(錯誤地)認為`map()`只提供一個參數,并且`parseInt()`只接受一個參數。那么你會驚訝于以下結果:
```js
> [ '1', '2', '3' ].map(parseInt)
[ 1, NaN, NaN ]
```
`map()`期望一個具有以下簽名的函數:
```js
function (element, index, array)
```
但是`parseInt()`有以下簽名:
```js
parseInt(string, radix?)
```
因此,`map()`不僅填充了`string`(通過`element`),而且還填充了`radix`(通過`index`)。這意味著前面的數組的值如下:
```js
> parseInt('1', 0)
1
> parseInt('2', 1)
NaN
> parseInt('3', 2)
NaN
```
總之,要注意那些你不確定其簽名的函數和方法。如果您使用它們,那么要明確知道接收哪些參數以及傳遞哪些參數是很有意義的。下面通過回調實現的:
```js
> ['1', '2', '3'].map(function (x) { return parseInt(x, 10) })
[ 1, 2, 3 ]
```
## 命名參數
在編程語言中調用函數(或方法)時,必須將實際參數(由調用者指定)映射到形式參數(函數定義)。有兩種常見的方法:
位置參數通過位置映射。第一個實際參數映射到第一個形式參數,第二個實際參數映射到第二個形式參數,依此類推。
命名參數使用名稱(標簽)來執行映射。名稱與函數定義中的形式參數相關聯,并在函數調用中標記實際參數。只要它們被正確地標記,命名參數出現的順序并不重要,。
命名參數有兩個主要優點:
它們為函數調用中的參數提供描述,它們也可以用于可選參數。我首先解釋好處,然后向您展示如何通過對象字面量模擬JavaScript中的命名參數。
### 命名參數的描述
一旦一個函數有多個參數,你可能會對每個參數的用途感到困惑。例如,假設您有一個函數`selectEntries()`,它從數據庫返回條目。給定以下函數調用::
```js
selectEntries(3, 20, 2);
```
這三個數字是什么意思?Python支持命名參數,使我們很容易弄清楚發生了什么:
```python
selectEntries(start=3, end=20, step=2) # Python syntax
```
### 可選的命名參數
可選的位置參數只有**在最后被省略時**才有效。在其他任何地方,您必須插入占位符,例如`null`,以便其余的參數具有正確的位置。對于可選的命名參數,這不是問題。你可以很容易地忽略其中任何一個。這里有些例子:
```python
# Python syntax
selectEntries(step=2)
selectEntries(end=20, start=3)
selectEntries()
```
### 在JavaScript中模擬命名參數
JavaScript不支持像`Python`和許多其他語言那樣的命名參數。但是有一個相當優雅的模擬:通過一個對象字面量來命名參數,作為一個單獨的實際參數傳遞。當您使用該技術時,`selectEntries()`的調用看起來是這樣的:
```js
selectEntries({ start: 3, end: 20, step: 2 }); //這是使用來對象字面量來模擬JS不支持的命名參數
```
該函數接收具有`start`, `end`, 和 `step`屬性的對象。你可以省略其中的任何一個:
```js
selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();
```
您可以像下面一樣實現`selectEntries()`:
```js
function selectEntries(options) {
options = options || {};
var start = options.start || 0;
var end = options.end || getDbLength();
var step = options.step || 1;
...
}
```
您還可以將位置參數與命名參數相結合。對后者來說,這是慣例:
```js
someFunc(posArg1, posArg2, { namedArg1: 7, namedArg2: true });
```
| **注意** |
| :----------: |
| 在JavaScript中,這里顯示的命名參數的模式有時被稱為*選項*或*選項對象*(例如:通過jQuery文檔)。 |
- 本書簡介
- 前言
- 關于這本書你需要知道些什么
- 如何閱讀本書
- 目錄
- I. JavaScript的快速入門
- 第1章 基礎的JavaScript
- II. 背景知識
- 第2章 為什么選擇JavaScript?
- 第3章 JavaScript的性質
- 第4章 JavaScript是如何創造出來的
- 第5章 標準化:ECMAScript
- 第6章 JavaScript的歷史里程碑
- III. 深入JavaScript
- 第7章 JavaScript語法
- 第8章 值
- 第9章 運算符
- 第10章 布爾值
- 第11章 數字
- 第12章 字符串
- 第13章 語句
- 第14章 異常捕獲
- 第15章 函數
- 第16章 變量:作用域、環境和閉包
- 第17章 對象和繼承
- 第18章 數組
- 第19章 正則表達式
- 第20章 Date
- 第21章 Math
- 第22章 JSON
- 第23章 標準全局變量
- 第24章 編碼和JavaScript
- 第25章 ECMAScript 5中的新功能
- IV. 技巧、工具和類庫
- 第26章 元代碼樣式指南
- 第27章 調試的語言機制
- 第28章 子類化內置構造函數
- 第29章 JSDoc:生成API文檔
- 第30章 類庫
- 第31章 模塊系統和包管理器
- 第32章 其他工具
- 第33章 接下來該做什么
- 著作權