ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[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文档)。 |