函数是可以调用的值。定义函数的一种方法称为 函数声明。 例如,以下代码定义了函数 id
,它有一个参数 x
:
function
id
(
x
)
{
return
x
;
}
return
语句从 id
返回一个值。 你可以通过提及函数名称,后跟括号中的参数来调用函数:
> id('hello') 'hello'
如果函数没有返回任何内容,则会(隐式地)返回 undefined
> function f() { } > f() undefined
本节只展示了一种定义函数和调用函数的方法。其他方法将在后面介绍。
你可以直接调用函数。 然后它就像一个普通函数一样工作。下面是一个调用示例:
id
(
'hello'
)
按照惯例,普通函数的名称以小写字母开头。
你可以通过 new
运算符调用函数。 然后它就变成了一个构造函数,一个对象的工厂。下面是一个调用示例:
new
Date
()
按照惯例,构造函数的名称以大写字母开头。
你可以将函数存储在对象的属性中,这会将其转换为 方法,你可以通过该对象调用该方法。 下面是一个调用示例:
obj
.
method
()
按照惯例,方法的名称以小写字母开头。
非方法函数将在本章中解释;构造函数和方法将在 第 17 章 中解释。
术语 参数 和 实参 通常可以互换使用,因为上下文通常可以清楚地表明其含义。以下是一条区分它们的经验法则。
参数 用于定义函数。 它们也被称为形式参数和形式实参。在以下示例中,param1
和 param2
是参数:
function
foo
(
param1
,
param2
)
{
...
}
实参 用于调用函数。 它们也被称为实际参数和实际实参。在以下示例中,3
和 7
是实参:
foo
(
3
,
7
);
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()
计算存储在字符串中的 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
;
};
call()
、apply()
和 bind()
是所有函数都拥有的方法(请记住,函数是对象,因此拥有方法)。 它们可以在调用方法时为 this
提供一个值,因此在面向对象的上下文中很有趣(请参阅 在设置 this 时调用函数:call()、apply() 和 bind())。本节解释了非方法的两个用例。
此方法在调用函数 func
时使用 argArray
的元素作为参数;也就是说,以下两个表达式是等效的:
func
(
arg1
,
arg2
,
arg3
)
func
.
apply
(
null
,
[
arg1
,
arg2
,
arg3
])
thisValue
是 this
在执行 func
时拥有的值。在非面向对象的设置中不需要它,因此这里它是 null
。
每当函数以类似数组的方式接受多个参数(但不是数组)时,apply()
就会很有用。
多亏了 apply()
,我们可以使用 Math.max()
(请参阅 其他函数)来确定数组的最大元素
> Math.max(17, 33, 2) 33 > Math.max.apply(null, [17, 33, 2]) 33
这将执行 部分函数应用——创建一个新函数,该函数调用 func
,并将 this
设置为 thisValue
,并将以下参数:首先是 arg1
到 argN
,然后是新函数的实际参数。在以下非面向对象的设置中不需要 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
仅存在于函数内部(包括方法)。 它是一个类数组对象,保存当前函数调用的所有实际参数。以下代码使用它:
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
转换为数组,如 类数组对象和泛型方法 中所述。
它是一个对象,因此所有对象方法和运算符都可用。 例如,你可以使用 in
运算符(属性的迭代和检测)来检查 arguments
是否“具有”给定的索引
> function f() { return 1 in arguments } > f('a') false > f('a', 'b') true
你可以以类似的方式使用 hasOwnProperty()
(属性的迭代和检测):
> function g() { return arguments.hasOwnProperty(1) } > g('a', 'b') true
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
(即,不变)。另一种可能性是将可选参数作为命名参数传入,作为 对象字面量的属性(请参阅 命名参数)。
在 JavaScript 中,您不能通过引用传递参数;也就是说,如果您将一个变量传递给一个函数,则会复制该变量的值并将其传递给该函数(按值传递)。因此,该函数无法更改该变量。如果需要这样做,则必须包装该变量的值(例如,包装在数组中)。
此示例演示了一个递增变量的函数
function
incRef
(
numberRef
)
{
numberRef
[
0
]
++
;
}
var
n
=
[
7
];
incRef
(
n
);
console
.
log
(
n
[
0
]);
// 8
如果您将函数 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
可选位置参数只有在末尾省略时才有效。在其他任何地方,您都必须插入占位符,例如 null
,以便其余参数具有正确的位置。使用可选命名参数,这不是问题。您可以轻松省略其中任何一个。以下是一些示例:
# Python syntax
selectEntries
(
step
=
2
)
selectEntries
(
end
=
20
,
start
=
3
)
selectEntries
()
JavaScript 本身不支持像 Python 和许多其他语言那样的命名参数。 但是有一种相当优雅的模拟方法:通过对象字面量命名参数,并将其作为单个实际参数传递。当您使用此技术时,selectEntries()
的调用如下所示:
selectEntries
({
start
:
3
,
end
:
20
,
step
:
2
});
该函数接收一个具有属性 start
、end
和 step
的对象。您可以省略其中任何一个
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
});