第 15 章 函数
目录
购买本书
(广告,请不要屏蔽。)

第 15 章 函数

函数是可以调用的值。定义函数的一种方法称为 函数声明 例如,以下代码定义了函数 id,它有一个参数 x

function id(x) {
    return x;
}

return 语句从 id 返回一个值。 你可以通过提及函数名称,后跟括号中的参数来调用函数:

> id('hello')
'hello'

如果函数没有返回任何内容,则会(隐式地)返回 undefined

> function f() { }
> f()
undefined

本节只展示了一种定义函数和调用函数的方法。其他方法将在后面介绍。

函数在 JavaScript 中的三个角色

定义了函数后(如上所示),它可以扮演多个角色:

非方法函数(“普通函数”)

你可以直接调用函数。 然后它就像一个普通函数一样工作。下面是一个调用示例:

id('hello')

按照惯例,普通函数的名称以小写字母开头。

构造函数

你可以通过 new 运算符调用函数。 然后它就变成了一个构造函数,一个对象的工厂。下面是一个调用示例:

new Date()

按照惯例,构造函数的名称以大写字母开头。

方法

你可以将函数存储在对象的属性中,这会将其转换为 方法,你可以通过该对象调用该方法。 下面是一个调用示例:

obj.method()

按照惯例,方法的名称以小写字母开头。

非方法函数将在本章中解释;构造函数和方法将在 第 17 章 中解释。

术语:“参数”与“实参”

术语 参数实参 通常可以互换使用,因为上下文通常可以清楚地表明其含义。以下是一条区分它们的经验法则。

定义函数

本节介绍三种创建函数的方法:

  • 通过函数表达式
  • 通过函数声明
  • 通过构造函数 Function()

所有函数都是对象,是 Function 的实例

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,并通过该变量调用它。函数表达式生成的值可以赋给变量(如上例所示),作为参数传递给另一个函数,等等。因为普通函数表达式没有名称,所以它们也被称为 匿名函数表达式

命名函数表达式

你可以给函数表达式一个名称。 命名函数表达式 允许函数表达式引用自身,这对于自递归很有用:

var fac = function me(n) {
    if (n > 0) {
        return n * me(n-1);
    } else {
        return 1;
    }
};
console.log(fac(3)); // 6

注意

命名函数表达式的名称只能在函数表达式内部访问

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

函数声明

以下是一个函数声明:

function add(x, y) {
    return x + y;
}

前面的代码看起来像一个函数表达式,但它是一个语句(请参阅 表达式与语句)。它大致等效于以下代码

var add = function (x, y) {
    return x + y;
};

换句话说,函数声明声明一个新变量,创建一个函数对象,并将其赋给该变量。

Function 构造函数

构造函数 Function() 计算存储在字符串中的 JavaScript 代码。 例如,以下代码等效于前面的示例:

var add = new Function('x', 'y', 'return x + y');

但是,这种定义函数的方法速度很慢,并且将代码保存在字符串中(工具无法访问)。因此,如果可能的话,最好使用函数表达式或函数声明。 使用 new Function() 计算代码 更详细地解释了 Function();它的工作原理类似于 eval()

提升

提升 意味着“移动到作用域的开头”。 函数声明被完全提升,而变量声明只被部分提升。

函数声明被完全提升。这允许你在声明函数之前调用它

foo();
function foo() {  // this function is hoisted
    ...
}

前面的代码之所以有效,是因为 JavaScript 引擎将 foo 的声明移动到了作用域的开头。它们执行代码,就好像它看起来像这样

function foo() {
    ...
}
foo();

var 声明也会被提升,但只提升声明,而不提升使用它们进行的赋值。 因此,如果像前面的示例那样使用 var 声明和函数表达式,则会导致错误:

foo();  // TypeError: undefined is not a function
var foo = function () {
    ...
};

只有变量声明被提升。引擎执行前面的代码,就像

var foo;
foo();  // TypeError: undefined is not a function
foo = function () {
    ...
};

函数的名称

大多数 JavaScript 引擎都支持函数对象的非标准属性 name 函数声明有它:

> function f1() {}
> f1.name
'f1'

匿名函数表达式的名称是空字符串:

> var f2 = function () {};
> f2.name
''

但是,命名函数表达式确实有名称:

> var f3 = function myName() {};
> f3.name
'myName'

函数的名称对于调试很有用。出于这个原因,有些人总是给他们的函数表达式命名。

哪个更好:函数声明还是函数表达式?

你应该更喜欢像下面这样的函数声明吗?

function id(x) {
    return x;
}

还是等效的 var 声明加函数表达式的组合?

var id = function (x) {
    return x;
};

它们基本上是一样的,但函数声明比函数表达式有两个优点:

  • 它们会被提升(请参阅 提升),因此你可以在它们出现在源代码之前调用它们。
  • 它们有名称(请参阅 函数的名称)。但是,JavaScript 引擎在推断匿名函数表达式的名称方面做得越来越好。

更好地控制函数调用:call()、apply() 和 bind()

call()apply()bind() 是所有函数都拥有的方法(请记住,函数是对象,因此拥有方法)。 它们可以在调用方法时为 this 提供一个值,因此在面向对象的上下文中很有趣(请参阅 在设置 this 时调用函数:call()、apply() 和 bind())。本节解释了非方法的两个用例。

func.apply(thisValue, argArray)

此方法在调用函数 func 时使用 argArray 的元素作为参数;也就是说,以下两个表达式是等效的:

func(arg1, arg2, arg3)
func.apply(null, [arg1, arg2, arg3])

thisValuethis 在执行 func 时拥有的值。在非面向对象的设置中不需要它,因此这里它是 null

每当函数以类似数组的方式接受多个参数(但不是数组)时,apply() 就会很有用。

多亏了 apply(),我们可以使用 Math.max()(请参阅 其他函数)来确定数组的最大元素

> Math.max(17, 33, 2)
33
> Math.max.apply(null, [17, 33, 2])
33

func.bind(thisValue, arg1, ..., argN)

这将执行 部分函数应用——创建一个新函数,该函数调用 func,并将 this 设置为 thisValue,并将以下参数:首先是 arg1argN,然后是新函数的实际参数。在以下非面向对象的设置中不需要 thisValue,这就是为什么它是 null

在这里,我们使用 bind() 创建一个新函数 plus1(),它类似于 add(),但只需要参数 y,因为 x 始终为 1

function add(x, y) {
    return x + y;
}
var plus1 = add.bind(null, 1);
console.log(plus1(5));  // 6

换句话说,我们创建了一个新函数,它等效于以下代码

function plus1(y) {
    return add(1, y);
}

处理缺少或多余的参数

JavaScript 不强制执行函数的元数:你可以使用任意数量的实际参数调用它,而与定义了哪些形式参数无关。因此,实际参数和形式参数的数量在两个方面可能有所不同:

实际参数多于形式参数
多余的参数将被忽略,但可以通过特殊的类数组变量 arguments 检索(稍后讨论)。
实际参数少于形式参数
缺少的形式参数的值都为 undefined

按索引获取所有参数:特殊变量 arguments

特殊变量 arguments 仅存在于函数内部(包括方法)。 它是一个类数组对象,保存当前函数调用的所有实际参数。以下代码使用它:

function logArgs() {
    for (var i=0; i<arguments.length; i++) {
        console.log(i+'. '+arguments[i]);
    }
}

以下是交互

> logArgs('hello', 'world')
0. hello
1. world

arguments 具有以下特点

arguments 的已弃用功能

严格模式删除了 arguments 的几个更不寻常的功能:

  • arguments.callee 指的是当前函数。它主要用于在匿名函数中进行自递归,并且在严格模式下是不允许的。作为一种解决方法,可以使用命名函数表达式(请参阅命名函数表达式),它可以通过其名称引用自身。
  • 在非严格模式下,如果您更改参数,arguments 会保持最新状态

    function sloppyFunc(param) {
        param = 'changed';
        return arguments[0];
    }
    console.log(sloppyFunc('value'));  // changed

    但在严格模式下不会进行这种更新

    function strictFunc(param) {
        'use strict';
        param = 'changed';
        return arguments[0];
    }
    console.log(strictFunc('value'));  // value
  • 严格模式禁止对变量 arguments 进行赋值(例如,通过 arguments++)。但仍然允许对元素和属性进行赋值。

强制参数,强制执行最小参数数量

有三种方法可以确定是否缺少参数。 首先,您可以检查它是否为 undefined

function foo(mandatory, optional) {
    if (mandatory === undefined) {
        throw new Error('Missing parameter: mandatory');
    }
}

其次,您可以将参数解释为布尔值。然后 undefined 被视为 false。但是,需要注意的是:其他几个值也被视为 false(请参阅真值和假值),因此该检查无法区分,例如,0 和缺少的参数

if (!mandatory) {
    throw new Error('Missing parameter: mandatory');
}

第三,您还可以检查 arguments 的长度以强制执行最小参数数量:

if (arguments.length < 1) {
    throw new Error('You need to provide at least 1 argument');
}

最后一种方法与其他方法不同

  • 前两种方法不区分 foo()foo(undefined)。在这两种情况下,都会抛出异常。
  • 第三种方法会为 foo() 抛出异常,并为 foo(undefined)optional 设置为 undefined

可选参数

如果参数是可选的,则表示如果缺少该参数,则为其指定默认值。 与强制参数类似,有四种选择。

首先,检查 undefined

function bar(arg1, arg2, optional) {
    if (optional === undefined) {
        optional = 'default value';
    }
}

其次,将 optional 解释为布尔值

if (!optional) {
    optional = 'default value';
}

第三,您可以使用 或运算符 ||(请参阅 逻辑或 (||)),如果左操作数不是假值,则返回左操作数。否则,它返回右操作数

// Or operator: use left operand if it isn't falsy
optional = optional || 'default value';

第四,您可以通过 arguments.length 检查函数的参数数量

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) 将其保留为 undefined(即,不变)。

另一种可能性是将可选参数作为命名参数传入,作为 对象字面量的属性(请参阅 命名参数)。

陷阱:意外的可选参数

如果您将函数 c 作为参数传递给另一个函数 f,则您必须注意两个签名:

  • f 期望其参数具有的签名。 f 可能会提供多个参数,而 c 可以决定使用其中多少个(如果有)。
  • c 的实际签名。例如,它可能支持可选参数。

如果两者不同,则可能会得到意外的结果:c 可能具有您不知道的可选参数,并且这些参数会错误地解释 f 提供的其他参数。

例如,考虑数组方法 map()(请参阅转换方法),其参数通常是一个具有单个参数的函数

> [ 1, 2, 3 ].map(function (x) { return x * x })
[ 1, 4, 9 ]

您可以作为 参数传递的一个函数是 parseInt()(请参阅 通过 parseInt() 获取整数

> parseInt('1024')
1024

您可能(错误地)认为 map() 仅提供一个参数,而 parseInt() 仅接受一个参数。 那么您会对以下结果感到惊讶:

> [ '1', '2', '3' ].map(parseInt)
[ 1, NaN, NaN ]

map() 期望具有以下签名的函数

function (element, index, array)

parseInt() 具有以下签名

parseInt(string, radix?)

因此,map() 不仅会填充 string(通过 element),还会填充 radix(通过 index)。这意味着前面数组的值的生成方式如下

> parseInt('1', 0)
1
> parseInt('2', 1)
NaN
> parseInt('3', 2)
NaN

总而言之,请谨慎使用您不确定其签名的函数和方法。如果您使用它们,则明确说明接收哪些参数以及传递哪些参数通常是有意义的。这是通过回调函数实现的

> ['1', '2', '3'].map(function (x) { return parseInt(x, 10) })
[ 1, 2, 3 ]

命名参数

在编程语言中调用函数(或方法)时,您必须将实际参数(由调用者指定)映射到形式参数(函数定义中的参数)。有两种常见的方法可以做到这一点:

命名参数有两个主要优点:它们为函数调用中的参数提供描述,并且它们适用于可选参数。我将首先解释这些优点,然后向您展示如何通过对象字面量在 JavaScript 中模拟命名参数。

命名参数作为描述

一旦函数具有多个参数,您可能会混淆每个参数的用途。例如,假设您有一个函数 selectEntries(),它从数据库中返回条目。给定以下函数调用:

selectEntries(3, 20, 2);

这三个数字是什么意思?Python 支持命名参数,它们可以很容易地弄清楚发生了什么

selectEntries(start=3, end=20, step=2)  # Python syntax

在 JavaScript 中模拟命名参数

JavaScript 本身不支持像 Python 和许多其他语言那样的命名参数。 但是有一种相当优雅的模拟方法:通过对象字面量命名参数,并将其作为单个实际参数传递。当您使用此技术时,selectEntries() 的调用如下所示:

selectEntries({ start: 3, end: 20, step: 2 });

该函数接收一个具有属性 startendstep 的对象。您可以省略其中任何一个

selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();

您可以按如下方式实现 selectEntries()

function selectEntries(options) {
    options = options || {};
    var start = options.start || 0;
    var end = options.end || getDbLength();
    var step = options.step || 1;
    ...
}

您还可以将位置参数与命名参数结合使用。 通常后者排在最后:

someFunc(posArg1, posArg2, { namedArg1: 7, namedArg2: true });

注意

在 JavaScript 中,此处显示的命名参数模式有时被称为选项选项对象(例如,在 jQuery 文档中)。

下一页:16. 变量:作用域、环境和闭包