函数是可以调用的值。定义函数的一种方法称为 函数声明。 例如,以下代码定义了函数 id,它有一个参数 x:
functionid(x){returnx;}
return 语句从 id 返回一个值。 你可以通过提及函数名称,后跟括号中的参数来调用函数:
> id('hello')
'hello'如果函数没有返回任何内容,则会(隐式地)返回 undefined
> function f() { }
> f()
undefined本节只展示了一种定义函数和调用函数的方法。其他方法将在后面介绍。
你可以直接调用函数。 然后它就像一个普通函数一样工作。下面是一个调用示例:
id('hello')
按照惯例,普通函数的名称以小写字母开头。
你可以通过 new 运算符调用函数。 然后它就变成了一个构造函数,一个对象的工厂。下面是一个调用示例:
newDate()
按照惯例,构造函数的名称以大写字母开头。
你可以将函数存储在对象的属性中,这会将其转换为 方法,你可以通过该对象调用该方法。 下面是一个调用示例:
obj.method()
按照惯例,方法的名称以小写字母开头。
非方法函数将在本章中解释;构造函数和方法将在 第 17 章 中解释。
术语 参数 和 实参 通常可以互换使用,因为上下文通常可以清楚地表明其含义。以下是一条区分它们的经验法则。
参数 用于定义函数。 它们也被称为形式参数和形式实参。在以下示例中,param1 和 param2 是参数:
functionfoo(param1,param2){...}
实参 用于调用函数。 它们也被称为实际参数和实际实参。在以下示例中,3 和 7 是实参:
foo(3,7);
Function()所有函数都是对象,是 Function 的实例
functionid(x){returnx;}console.log(idinstanceofFunction);// true
因此,函数从 Function.prototype 获取其方法。
varadd=function(x,y){returnx+y};console.log(add(2,3));// 5
前面的代码将函数表达式的结果赋给变量 add,并通过该变量调用它。函数表达式生成的值可以赋给变量(如上例所示),作为参数传递给另一个函数,等等。因为普通函数表达式没有名称,所以它们也被称为 匿名函数表达式。
你可以给函数表达式一个名称。 命名函数表达式 允许函数表达式引用自身,这对于自递归很有用:
varfac=functionme(n){if(n>0){returnn*me(n-1);}else{return1;}};console.log(fac(3));// 6
命名函数表达式的名称只能在函数表达式内部访问
varrepeat=functionme(n,str){returnn>0?str+me(n-1,str):'';};console.log(repeat(3,'Yeah'));// YeahYeahYeahconsole.log(me);// ReferenceError: me is not defined
functionadd(x,y){returnx+y;}
前面的代码看起来像一个函数表达式,但它是一个语句(请参阅 表达式与语句)。它大致等效于以下代码
varadd=function(x,y){returnx+y;};
换句话说,函数声明声明一个新变量,创建一个函数对象,并将其赋给该变量。
构造函数 Function() 计算存储在字符串中的 JavaScript 代码。 例如,以下代码等效于前面的示例:
varadd=newFunction('x','y','return x + y');
但是,这种定义函数的方法速度很慢,并且将代码保存在字符串中(工具无法访问)。因此,如果可能的话,最好使用函数表达式或函数声明。 使用 new Function() 计算代码 更详细地解释了 Function();它的工作原理类似于 eval()。
提升 意味着“移动到作用域的开头”。 函数声明被完全提升,而变量声明只被部分提升。
函数声明被完全提升。这允许你在声明函数之前调用它
foo();functionfoo(){// this function is hoisted...}
前面的代码之所以有效,是因为 JavaScript 引擎将 foo 的声明移动到了作用域的开头。它们执行代码,就好像它看起来像这样
functionfoo(){...}foo();
var 声明也会被提升,但只提升声明,而不提升使用它们进行的赋值。 因此,如果像前面的示例那样使用 var 声明和函数表达式,则会导致错误:
foo();// TypeError: undefined is not a functionvarfoo=function(){...};
只有变量声明被提升。引擎执行前面的代码,就像
varfoo;foo();// TypeError: undefined is not a functionfoo=function(){...};
大多数 JavaScript 引擎都支持函数对象的非标准属性 name。 函数声明有它:
> function f1() {}
> f1.name
'f1'> var f2 = function () {};
> f2.name
''> var f3 = function myName() {};
> f3.name
'myName'函数的名称对于调试很有用。出于这个原因,有些人总是给他们的函数表达式命名。
functionid(x){returnx;}
还是等效的 var 声明加函数表达式的组合?
varid=function(x){returnx;};
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
functionadd(x,y){returnx+y;}varplus1=add.bind(null,1);console.log(plus1(5));// 6
换句话说,我们创建了一个新函数,它等效于以下代码
functionplus1(y){returnadd(1,y);}
JavaScript 不强制执行函数的元数:你可以使用任意数量的实际参数调用它,而与定义了哪些形式参数无关。因此,实际参数和形式参数的数量在两个方面可能有所不同:
arguments 检索(稍后讨论)。undefined。特殊变量 arguments 仅存在于函数内部(包括方法)。 它是一个类数组对象,保存当前函数调用的所有实际参数。以下代码使用它:
functionlogArgs(){for(vari=0;i<arguments.length;i++){console.log(i+'. '+arguments[i]);}}
以下是交互
> logArgs('hello', 'world')
0. hello
1. worldarguments 具有以下特点
它是类数组的,但不是数组。 一方面,它有一个属性 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')
truearguments.callee 指的是当前函数。它主要用于在匿名函数中进行自递归,并且在严格模式下是不允许的。作为一种解决方法,可以使用命名函数表达式(请参阅命名函数表达式),它可以通过其名称引用自身。在非严格模式下,如果您更改参数,arguments 会保持最新状态
functionsloppyFunc(param){param='changed';returnarguments[0];}console.log(sloppyFunc('value'));// changed
但在严格模式下不会进行这种更新
functionstrictFunc(param){'use strict';param='changed';returnarguments[0];}console.log(strictFunc('value'));// value
arguments 进行赋值(例如,通过 arguments++)。但仍然允许对元素和属性进行赋值。有三种方法可以确定是否缺少参数。 首先,您可以检查它是否为 undefined:
functionfoo(mandatory,optional){if(mandatory===undefined){thrownewError('Missing parameter: mandatory');}}
其次,您可以将参数解释为布尔值。然后 undefined 被视为 false。但是,需要注意的是:其他几个值也被视为 false(请参阅真值和假值),因此该检查无法区分,例如,0 和缺少的参数
if(!mandatory){thrownewError('Missing parameter: mandatory');}
第三,您还可以检查 arguments 的长度以强制执行最小参数数量:
if(arguments.length<1){thrownewError('You need to provide at least 1 argument');}
最后一种方法与其他方法不同
foo() 和 foo(undefined)。在这两种情况下,都会抛出异常。foo() 抛出异常,并为 foo(undefined) 将 optional 设置为 undefined。如果参数是可选的,则表示如果缺少该参数,则为其指定默认值。 与强制参数类似,有四种选择。
首先,检查 undefined
functionbar(arg1,arg2,optional){if(optional===undefined){optional='default value';}}
其次,将 optional 解释为布尔值
if(!optional){optional='default value';}
第三,您可以使用 或运算符 ||(请参阅 逻辑或 (||)),如果左操作数不是假值,则返回左操作数。否则,它返回右操作数
// Or operator: use left operand if it isn't falsyoptional=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 中,您不能通过引用传递参数;也就是说,如果您将一个变量传递给一个函数,则会复制该变量的值并将其传递给该函数(按值传递)。因此,该函数无法更改该变量。如果需要这样做,则必须包装该变量的值(例如,包装在数组中)。
此示例演示了一个递增变量的函数
functionincRef(numberRef){numberRef[0]++;}varn=[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 syntaxselectEntries(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()
functionselectEntries(options){options=options||{};varstart=options.start||0;varend=options.end||getDbLength();varstep=options.step||1;...}
您还可以将位置参数与命名参数结合使用。 通常后者排在最后:
someFunc(posArg1,posArg2,{namedArg1:7,namedArg2:true});