[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章 接下来该做什么
- 著作权