函数
撰写时间:2023-10-15
修订时间:2026-05-17
参数
arguments
在函数方法体内,有一特殊的变量arguments,用于接收所有传入的实参,即使没有相应的形参。
在非严格环境下,arguments有一名为callee的属性,表示arguments所属的当前执行函数。
但该属性已被废弃,因此尽量不要使用。
判断空值
undefined, null
当向函数的形参传递实参时,可能会出现几种情况。
第一种情况:未传入实参。
此时在函数体内,形参param的值为undefined。
第二种情况:也可显式地传入undefined值。
此时函数体内的形参值与上面一样,也为undefined。
第三种情况:传入null值。
共有2个形参,但向第2个形参显式地传入null值,表示没有该值。
此时在函数体的形参param的值为null。
注意:当实参后面有需要传入的实参时,该实参不能省略,即不能为testParams(25, , 30)的形式,而应在该位置显式地传入null值。
值为null或undefined的形参均可用!语句来判断:
形参的默认值
需特别注意的是,如果函数形参有默认值,则:
当实参传入null值时,形参param2的默认值不会生效。null值视为显式地指定了特定值,因此会覆盖默认值。
而由于null
的字符数比undefined
少,因此客户端总喜欢偷懒地写成:
这将导致很容易出现不易觉察的 bugs 。
代码重复,且带有默认值的形参形同虚设。
因此,如果允许客户端的实参传来null值,则建议干脆取消实参的默认值,改为在函数体内使用!操作符判断后手工设置默认值:
如果形参param2的可取值为简单的数值,则可使用??=操作符来赋值:
数组
对于数组来讲,有下列3种情况:
结果分别为:
但对于空数组,不能使用!语句来判断其是否为空:
而应使用:
如果函数中类型为数组的参数有默认值时:
结果为:
即,只有在值为undefined
时,默认值才会生效。而如果传入null
值,则默认值不会生效。
因此,对于类型为数组的形参,最好不要设置默认值。相反,应在函数体中进行判断:
上面的代码,则将3种情况下的数组都进行了统一的初始化。
导致出现上面各种复杂情况的原因在于,我们希望尽可能简化客户端的代码,即,应允许客户端灵活地进行调用:
而为了应付这些灵活的调用情况,就不得不在函数体中进行全面的判断。
可变长参数
JavaScript的函数支持可变长参数,这种函数称为参数可变长函数(variadic function)。
形参...nums将所有可变长的实参a, b, c都打包为一个数组。
在JavaScript中,形参...nums被称为rest parameter(剩余参数)。
rest parameter的含义是指,排在其前面的参数可明确指出,剩余的部分可自动收纳进来。
形参前面2个参数是固定的,而最后的...nums则将剩余的实参都打包在一起(不包含前面两个固定的参数)。
注意,当与有固定的参数混用时,rest parameter必须放在最后面。
下面是一个容易犯晕的例子。
在函数原型声明中,形参...args是可变长参数,而在该函数体中,args是一个数组,也即将可变长参数的各个值打包为一个数组。如果我们希望以args作为参数调用console.log方法,由于该方法的参数也是可变长参数,因此我们不能直接调用:
我们须将此数组使用展开操作符...展开为多个独立的个体后,再调用:
内嵌函数
内嵌函数是JavaScript的一大特色。
如果某函数只在一个地方使用,而又想与其他局部变量相互影响,则可像上面一样创建一个内嵌函数。它的好处是,对外部不可见,而又与内部的其他变量相互隔离。
自调用函数
基本形式
在声明函数时,可立即调用该函数。
上面,先声明了一个名为testFunc的函数,然后,将该函数的定义部分全部用()
包围起来,最后,像调用其他函数一样,在该函数名后面再加上()
来调用它。
因为我们无需使用函数名称来调用它,因此也可以采用匿名函数的方式:
更通用的写法如下所示:
或者,改用lambda方式:
自调用函数的变体
我们可以将一个自调用函数赋值给一个变量。
当使用这种方式时,可以省略第一组()
。
初始化静态变量
我们可以利用自调用函数的特点,实现静态变量的初始化。看下面的的代码:
变量genId是一个自调用函数。在其函数体中,声明了一个名为id的变量,然后,返回一个函数,此函数返回id的值后,再将其值加1。
当完成声明后,类似于以下的代码:
上面,以{}
包围起来的部分称为闭包(closure)。闭包定义了一个上下文环境,闭包内的变量只对闭包内的对象可见,而闭包外的对象则不可以访问闭包内的变量。
无论是使用自调用函数,还以使用闭包,都可以实现上面的功能。但闭包是独立的、没有封装性的,如果使用太多的闭包,则维护代码时,我们很容易迷失。而使用自调用函数的格式,具备了很好的封装性。
将变量id进行封装后,genId所返回的函数并未重新初始化该变量,因此,变量id成了名副其实的静态变量。
调用代码:
自调用函数的参数
自调用函数也可具备形参,且实际传入实参。
许多开源的JavaScript框架在初始化时都使用了类似于此的特性:
立即执行异步函数
有时如果需要立即执行异步函数,可使用async来实现。但其又需要在函数签名之前及调用相应的异步函数时使用await关键字,较为啰唆。此时,使用自调用函数的方式,可大大简化编码。
上面代码,加载、解析、并赋值于变量obj后,才执行下一行语句,因此,下一行语句可放心地访问obj了。
递归函数
递归算法需考虑以下因素:
- 必须有一个停止的条件
- 深度优先还是广度优先
- 先实现遍历,再实现数据的提取
- 使用数组保存递归的返回值
深度优先还是广度优先
深度优先的代码。
广度优先的代码。
如果需要对返回数据进行组装,则深度优先。
call, apply and bind
每个Function都有以下方法:
- call
- apply
- bind
call, apply及bind均是与在被调用的函数内如何设置神奇的this有关的方法。
call
call的基本机制
先看下面的代码:
在函数utils内部,打印this的值。
哪来的this?因此终端中输出undefined
不足为奇。但再加一行代码就可看出端倪:
如果this与a一样,则应都是ReferenceError异常的错误。可见,这两个变量情况还不一样。this这个变量是存在的,但只是尚未传进来而已。
在JavaScript中,每个函数,如同上面的utils,都有一个名为call的方法。当调用某个函数的call方法时,其第1个参数将成为该函数内部的this。
代码:
以person及car作为参数,调用函数utils的方法call,这样,在utils中,this将被赋值为此参数。
终端输出:
注意这里的参数传递机制。
函数utils没有任何形参,但却可以通过固定命名的方式,使用专门的、隐藏的变量this来接收不同的实参!
但并不是所有的函数调用都有这种能与神秘的this取得联系的特权。只有函数的call及apply方法才有这种特权。这就是它们存在的意义所在。
函数utils中this的意义
函数utils中这个特殊的变量this可不是一个普通的变量。它是可以变魔术的,可立即变为所有任意传进来的对象自身。
这意味着,utils这个环境已经立即转变为this所指向的对象的自身的环境。或者说,它已经被神灵附体了,被所传进的对象这个神灵附体了。它变成了这个神灵自己。
先以person为参数调用call:
utils的环境变成了person自己所在的环境。
再以car为参数调用call:
utils的环境变成了car自己所在的环境。
即,如果我们调用任意一个函数的call方法,则这个函数内部将立即变为一块神灵圣地。
鉴于其特点,我们可在utils函数中设计一些通用的算法:
可变长的其他参数
call方法的参数中,从第2个参数开始为可变长的参数。可用这个可变长的参数来投喂utils函数所声明的形参。
call方法的原型如下:
- ObjectthisArg
- ... *args
- thisArg
- 在被调用函数的内部,
this应指向的对象。 - ... args
- 被调用函数的其他参数。
apply
apply与call的作用一样,只不过是其他参数不是可变长的参数,而是打包进一个数组中。
apply方法的原型如下:
- ObjectthisArg
- *[]args
- thisArg
- 在被调用函数的内部,
this应指向的对象。 - args[]
- 被调用函数的其他参数。
应该是,apply方法是JavaScript早期就出现的方法,而随着JavaScript的演化,后面支持了可变长的参数,就又多了一个call方法。这两个方法功能一样,但只是参数的组织形式不同。使用call更加方便。
bind
先看一个例子:
要点:obj的方法getName方法中通过this来访问自己的属性name。
直接调用obj的getName方法,没有任何问题。
现在,声明一个指向obj的getName方法的引用,或称函数指针。然后再调用它。
程序将抛出TypeError异常:
意为当执行到getName方法中的this.name时,this的值为undefined,因而不能访问其name属性。
两种调用方法的区别在于运行环境不同。当JavaScript遇到obj.时,其运行环境是obj,此时可访问this.getName;而当JavaScript遇到funcPointer.时,其运行环境是全局的window,此时this为undefined。
此时,可调用funcPointer的bind方法,为该函数绑定一个在其内部若出现this时,this应指向的对象。
上面,代码
表示,在boundPointer这个运行环境中,将funcPointer内部的this绑定到obj上面。这样就可以直接调用boundPointer函数了。
何时使用bind方法?
- 当我们声明了一个变量,该变量指向一个函数
- 这个函数内部使用了
this
函数类
创建函数对象实例
看一个函数:
我们声明了一个名为sum的函数。这是我们所熟悉的方式。除此之外,我们还可以通过Function构造器来创建这个函数。
JavaScript中,使用构造器来创建对象的前面的new可以省略,因此也可写成:
Function构造器的参数均为字符串,最后一个参数是函数体内的内容,其前面是可变长的函数参数。上面a
与b
为参数的名称。
使用字符串作为函数体的内容,意味着在我们调用该函数时,JavaScript将解析此字符串、将此字符串转化为代码并运行,这为我们避免直接调用eval()提供了另一种较好的解决方案。
自动运行字符串
上面代码因为Function构造器只有一个字符串参数,它将成为函数体的内容。运行上面的代码,成功地运行了src的内容。
其效果等于:
简化它:
访问变量
可以在函数体内访问变量。
访问全局变量
因为src的内容是个字符串,我们可以使用字符串模板来访问变量。
num1及num2是全局变量,访问没问题。
访问局部变量
在函数中访问全局变量
在函数中照样可以访问全局导入的模块。
访问对象
由于安全机制的原因,当访问对象时,将有点问题。
将出现语法异常:
将${}去掉也不行:
这回是引用对象错误的异常:
对于第一种错误,这是Function构造器出于安全机制的考虑而不同于eval的地方,Function构造器不允许随便引用其他对象。
对于第二种错误,虽然在函数体内可以安全地引用对象,但目前函数体并未出现obj的身影。传入参数即可引用,详见下节。
传入参数
基本形式
使用下面的代码传入参数:
等同于:
注意,上面的a
与b
并不代表两个字符串,而是代表参数列表中共有2个参数,其参数名称分别为a与b。src作为函数体的字符串表示,直接引用了这两个参数名称。
小结一下:在函数体内所需要引用的对象名称,需出现在Function构造器的参数列表中,且该名称用单引号或双引号括起来。
现在,解决上一节所出现的问题:
多个参数
有点晃眼,不好理解。
抽象为通用的函数
对于需要传入多个参数的,我们可将其抽象为一个通用的函数。
将代表形参的params与代表函数体的body放在一起,很容易就可在大脑中脑补出这个函数的原型。形参params与实参args都打包为数组。
函数safeEval是我们抽象后得出的函数,在该函数中,将params与args通过...展开符展开为可变长的变量,再投喂给Function构造器即可。
小结
正常情况下,我们很少会编写Function构造器的代码。但存在这样的场景:我们需要将页面上特定文本框中的内容取出来,作为JavaScript的代码来执行。此时,最直接的方式就是调用eval函数。但该函数充满风险,效率也极其低下。此时,使用Function构造器来实现目标则可成为一种很好的选择。此时,了解并掌握Function构造器的相关细节就很有必要了。
获取函数或对象方法的源代码
函数源代码
可以直接打印特定函数,可查看其源代码:
但console.log方法没有返回值。为获取函数的源代码,可显式地调用其toString方法。这样,我们就持有了该函数的源代码。
实际上,在各浏览器的实现中,当在特定函数上应用console.log方法时,先在后台调用了该对象的toString方法后再打印。
对象方法的源代码
对于对象中的方法,可像上面一样直接打印该对象的特定方法的源代码。
但如果直接打印整个对象,则对象中各个方法的内容将被隐藏起来。
此时,可使用for ... in语句以遍历对象各属性的名值,再分别打印它们的内容。
上面在pc.log方法中,属性名值以逗号,
相隔开,这样在打印时可区别看到各属性值不同数据类型的效果。缺点是所打印的各个值之间,默认使用空格隔开,看着有点别扭。
如果使用格式字符串,则可完全依特定要求来定制输出内容:对于属性名,统一使用标识符%s
;对于属性值,因数据类型不一样,可不使用某种标识符来限定。
Safari在打印多个值时,不是以默认的空格符来分隔,而是以 -
号来分隔,从而破坏了自定义标识符%s
的效果。
我们也可通过调用Object.entries方法来直接获得对象各个属性名值。
