为了提升代码的可维护性和可阅读性等,我们会把 js 代码进行抽象和封装为一个个单独的函数,每一个函数只负责完成一个功能。
对于 js 中的函数的声明方式而言,有下面几种分类:
- 匿名函数:一般作为回调函数存在
- 具名函数:通过
function name() {}
方式声明的具有函数名的函数 - 函数表达式:将函数赋值给一个变量
- 立即执行函数:声明后立即执行的函数,
本文不研究 js 中各种函数声明方式和使用方式的区别,而是说说函数的参数。
Javascript 中函数的特性
不存在函数的重载
在其他语言中,定义一个函数时,需要定义好这个函数要使用到的参数。如果两个函数的函数名相同,而参数数量、类型不同时,会根据实际使用时传入的参数数量和类型来确定使用的是哪一个函数,并不会出现错误。而如果调用函数时,传入的参数与现有的改函数的参数无法完全匹配(少传或多传),就会出现错误。这种函数名相同参数不同的函数叫做函数重载。
但是 js 中并不存在函数重载这个概念。
js 函数的不定参数特性
但是 js 中,如果出现两个同名函数,即使参数数量不同,后面的函数也会覆盖前面的函数。即使传入的参数与函数定义的参数不匹配,也并不会报错,这是因为 js 中,函本身是不定参数长度的,对于定义函数时已经定义的参数,我们可以直接通过参数名获取到;但如果不传,那对应位置的参数就会是undefined
;如果多传,虽然我们不能够直接通过参数名获取到参数,但依然可以在函数内部获取到。
在函数内部不通过参数名获取参数
每个函数内部都会有一个 arguments
的变量,这个变量是一个数组,用来保存当前函数所接收到的所有参数。
function func1 (a, b, c) {
console.log(arguments)
console.log(arguments[0] === a)
}
func1(1, 2, 3, 4)
// [1, 2, 3, 4]
// true
在上面的例子中,arguments[0]
就是参数 a
的值,以此类推,而 arguments[3]
在参数中并没有与之相对应的参数名,所以无法通过参数名获取到这个参数,如果需要使用这个参数,只能通过 arguments[3]
的方式。
通过上面的解释,我们可以想到,虽然 js 中不存在重载,但是我们却可以通过判断
arguments
的长度来模拟函数的重载。
形参与实参
在学习 C 语言的时候,第一次接触到形参实参的概念,当时并不能够很好的理解这两个词的意思。
形参是形式参数(parameter)的缩写,而实参则是实际参数(argument)的缩写。看下面这个例子:
let e = 5
function func2 (d) {
d = 6
}
func2(e)
在上面的代码中,func2
函数具有一个参数 d
,当我们在函数体中使用这个参数时,会通过参数名 d
调用。当我们声明一个函数后,即使不调用这个函数,也就是没有 func2(e)
这一行代码,func2
的声明也会被执行后进入内存。此时执行 func2
时,参数 d
并不具有真正的值,它只相当于一个占位符,我们把它叫做形参。
当调用这个函数时,传入了变量 e
的值 5
,此时函数体中的 d
就具有了值,我们就把这个值 5
叫做实参。
(其实也可以直接用它们的英文来理解,声明函数时,参数叫 parameter,但是在函数内部获取参数时却不是用的 parameters,而是 arguments,刚好与上文相对应)
那么到这里问题就来了,明明传进去的是 e
,为什么实参不是 e
而是它的值 5
呢?要解释这个问题,先要搞清楚另一个问题。
函数参数的调用类型
还是如上面代码中,当函数体中改变参数的值时 d = 6
,外部变量 e
会不会也变成 6
?
关于这一点,我们需要先了解值类型与引用类型这两个类型的变量,小伙伴们可以参考之前的一篇文章: 前端面试题——对象的深浅拷贝
与值类型和引用类型相似的,函数中参数的调用也分为两种:值调用和引用调用,分别对应值类型和引用类型。
值类型的变量传入后,函数其实是将这个变量的值拷贝了一份然后使用(所以实参是 5
不是 e
),所以在函数中改变值类型变量的值,并不会导致外部变量的改变(所以上面例子中的 e
的值并不会被改变)。
而如果传入的参数是一个引用类型的变量,函数中存在的并不是这个变量的值的拷贝,而是这个变量内存地址的引用(这个情况下,实参就是这个变量),如果在函数中改变参数的某一个属性,则外部变量的属性也会改变:
// f 是一个引用类型变量,它指向一块内存中的地址
let f = {
g: 7
}
function func3 (obj) {
// obj 在接收到引用类型实参 f 后,指向的同样是 f 指向的地址
// 可以理解为给 obj 赋值为 f:obj = f,这样修改 obj 的属性后,f 的属性也会跟着改变
obj.g = 8
// 但如果完全修改 obj 的值:obj = 8,就不会影响外部变量的值,因为这等于是将 obj 的指向改变了,不再与外部变量具有相同的指向
}
func3(f)
console.log(f.g)
// 8
那如果在传入一个引用类型的变量时,不想外部的变量在函数中被默默改变该怎么办呢?方法很简单,将它克隆一份,函数中使用克隆出来的新的变量。关于这一点,依然可以参考上面对象深浅拷贝的那篇文章。当然,很多第三方库也都提供了这样的方法,比如 $.extend()
,而如果需要拷贝的对象层次不深,也可以直接使用Object.assign()
方法。
网友评论