# 函数的形参和实参

相信大家对函数的形参和实参都应该比较熟悉了.今天我们主要是来回顾一下其中的知识点,温故而知新,可以为师矣.最近的文章基本都是我在整理自己以前的笔记时,看到一些知识点的回顾总结.

# 形参和实参是什么

形参是形式参数(parameter),是指在函数定义时,预先定义用来在函数内部使用的参数 实参是实际参数(arguments),是指在函数调用时,传入函数中用来运算的实际值

function fn(a){  
  console.log(a)
}
fn('str')

上面代码中,fn后面的这个定义在()中的a就是形参,类似于一个占位符,占了第一个坑,代表了第一个传进来的参数.而下面函数调用时的'str'则是我们传进去的实际参数,是一个真正有意义的值.

# 形参和实参数量不相等的情况

在js中,形参和实参的数量往往可以是不相等的

function add(a,b){
  console.log(a+b)
}
add(1,2)  // 3
add(1)  // NaN
add(1,2,3)  // 3

第一次调用的时候,形参和实参的数量相等,完美计算出结果3 第二次调用的时候,实参比形参少了一个,结果为NaN,这是因为此时的形参b因为没有值而变成了undefined,在做加法运算的时候,undefined转为数值类型后为NaN,而NaN再加1,结果还是NaN. 第三次调用的时候,实参比形参多了一个,结果为3.这是因为当实参比形参多的时候,多余的实参会被忽略掉.类似于我这原来只有两个坑位,你们三个人一起来,那最后那个人只能没有坑位了.

# arguments对象

说到arguments对象,大家应该都比较熟悉了.因为平时的日常开发中也会经常使用到这个对象.arguments的样子有点像一个数组,但又不是真正的数组.所以我们叫它是一个类数组对象.除了具有length属性和索引信息外,arguments对象不具有其他数组的特性.像数组的push,splice等,它通通都没有.

function fn(){
  let len = arguments.length
  for(let i = 0; i < len; i++){
    console.log(arguments[i])
  }
}
fn(1,2)  // 1 2
fn(2,3,4)  // 2 3 4

arguments代表了传进函数的实参集合.我们可以通过遍历它来获取所有的实参. 其中,这个对象中的第一个元素和函数的第1个形参是对应的,以此类推.

function fn(a,b){
  console.log(a === arguments[0])
  console.log(b === arguments[1])
}
fn(1,2)  // true true

当我们改了形参的值,arguments对应的值也会发生改变.同样,改变了arguments的值,函数内的形参表示的值也发生了改变.

function fn(a){
  a = 2 
  console.log(arguments[0])  // 2
}
fn(1)

function fn(a){
  arguments[0] = 2 
  console.log(a)  // 2
}
fn(1)

注意了上面是在形参和实参的个数相同的情况下,形参和arguments对象中的值保持一致.当形参没有对应实参的时候,形参和arguments对象中的值并不对应

function fn(a){
  console.log(a === arguments[0])  // true
  a = 1 
  console.log(a === arguments[0])  // false
}
fn()

当然了,arguments对象既然作为一个类数组对象,也是可以转换为数组形式的.

function fn(){
  console.log(Array.from(arguments))
}
fn(1,2)  // [1,2]

# 如何让函数的形参和实参保持相等

其实在JS中函数参数的接受还是比较松散的.可以多传递一个值,也可以少传递一个值.这样难免有时候会引起不必要的BUG,那么我们该如何解决呢? 首先我们要介绍两个长度length属性.其中一个是函数的length,另外一个是argumentslength.它们分别表示函数形参的个数和实参的个数.

function fn(a){
  console.log(arguments.length)
}
fn(1,2)  // 2
console.log(fn.length)  // 1

既然我们能知道函数的形参个数和实参个数,那问题就好解决了

function fn(a){
  if(fn.length !== arguments.length){
    throw new Error('参数个数不对')
  }
  console.log('参数个数对的')
}
try{
  fn(1,2)
}catch(e){
  console.log(e)
}
fn(1)  // 参数个数对的

只要我们在函数的开头判断一下形参的个数和实参的个数是不是相等就ok了.

# 函数重载?

我们知道,JS中是没有传统意义上的函数重载的,后面定义的同名函数会覆盖前面的同名函数.

function fn(a){
  console.log(1)
}
function fn(a,b){
  console.log(2)
}
fn(1)  // 2
fn(1,2)  // 2

但是我们还是有办法可以通过arguments对象来模拟实现一个函数重载的功能

function fn(a){
  if(arguments.length === 1){
    console.log(1)
  }else{
    console.log(2)
  }
}
fn(1)  // 1
fn(1,2)  // 2

# 改变形参会不会对实参产生影响?

function fn(a){
  arguments[0] = 2 
  console.log(a) 
}
let a = 1
fn(1)  // 2
console.log(a)  // 1

上面的示例中,我们传入的是基本类型值,发现无论我们怎么修改形参,都不会影响到实参. 下面我们要传入一个引用类型的值,看它是否会受影响

function fn(obj){
  obj.name = 'lisi'
  obj = {
    name:'zhangsan',
    age:12
  }
  console.log(obj)
}

let obj = {}
fn(obj)  // {name: "zhangsan", age: 12}
console.log(obj)  // {name: "lisi"}

可以看出来,在函数里面操作形参影响了实参. 那么到底形参会不会影响到实参呢,这个答案我们可以从红宝书的传递参数一节找到答案. 这是因为,JavaScript所有函数参数都是按值传递.这就是答案,可是我擦,这话该怎么理解,也太抽象了吧!当我的参数是对象的时候,我怎么看起来更像是引用传递啊.想当初,看见这句话的时候,我是百思不得其解啊,它也没个形象点的解释,后面翻阅了一些资料,总算是有点理解了.具体的内容就不展开了,只要记住这里的按值传递,基本类型的值指的是本身,而引用类型的值指的是内存中的地址.我们来看下面的几个例子,证明一下刚刚的结论.

let a = 1
function fn(a){
  a = 2
}
fn(a)
console.log(a)  // 1

let obj = {}
function fn2(obj){
  obj.name = 'zhangsan'
}
fn2(obj)
console.log(obj)  // {name: "zhangsan"}

let obj2 = {}
function fn3(obj){
  obj = {
    name:'lisi'
  }
}
fn3(obj2)
console.log(obj2)  // {}

这里的fn3函数,我们可以理解为实际上是这样的一段代码

function fn3(obj){
    var obj = obj2  // 多了这里一步
    obj = {
        name:'lisi'
    }
}

上面多出来的一步,其实就是引用赋值的过程,拷贝的是对象的内存地址.对象的实际值保存在堆中,而栈中保存的是堆的内存地址.所以这里是将obj2的内存地址(类似 0x001234abcd)这一字符串赋值给了obj,但是obj又重新赋值了一个对象,所以在内存中开辟了一块新的内存地址.所以在执行完函数以后,再次打印obj2的值还是{}.结合上面的第二段代码来看,因为函数内的obj和函数外的obj指向的是同一内存地址.因此在函数内部给obj添加了name属性,会反映到函数外部的obj对象中.好了,我们再来念一遍这个结论:JavaScript所有函数参数都是按值传递,基本类型的值指的是本身,而引用类型的值指的是内存中的地址

# 函数默认参数

有时候,我们可以给函数设置一个默认参数.这样当外部没有实参传递进来的时候,我们就可以使用默认参数来进行运算.

function fn(a,b){
  b = b || 'zhangsan'
  console.log(a + ' ' +b)
}
fn('hello')  // hello zhangsan
fn('hello', false)  // hello zhangsan

不对啊,这里我明明在第二次调用的时候,传入了两个参数,为啥还是取用了默认值呢?这是因为 ||运算符当前面的值为假值的时候,会取后面的值作为结果计算.而当我们的实参传入类似undefined,null,NaN等假值时,上面设置默认参数的弊端就显示出来了. 因此我们最好用ES6中设置默认参数的方式来设置

function fn(a,b = 'zhangsan'){
  console.log(a + ' ' +b)
}
fn('hello')  // hello zhangsan
fn('hello', false)  // hello false

# 函数剩余参数(rest参数)

rest参数也是ES6新增加的,语法形式为...变量名,用于获取函数多余的参数.注意剩余参数后面不能再跟其他的参数了,否则会报错.

function fn(a,...rest){
  console.log(a)
  console.log(rest)
}
fn(1)  // 1 []
fn(1,2)  // 1 [2]
fn(1,2,3)  // 1 [2,3]
console.log(fn.length)  // 1

并且我们可以看到,方法fnlength并不包括rest参数. rest参数是一个真正的数组,可以使用数组原型上的所有方法,这点和arguments是不同的.此外arguments上还有callee.这个就是我们接下来要讲的内容了.

# arguments.callee

arguments.callee属性包含当前正在执行的函数. 下面是一个典型的算阶乘的函数

function fn(n){
  if(n < 2){
    return 1
  }else{
    return n * arguments.callee(n - 1)
  }
}
console.log(fn(1))  // 1
console.log(fn(4))  // 24

上面的arguments.callee在函数名称是未知的时候,是很有用的.但是ES5中规定了,在严格模式下是不准使用arguments.callee的,可以在这里找到原因.当这个函数必须调用自身的时候,可以使用函数声明或者命名一个函数表达式.

function fn(n){
  'use strict'
  if(n < 2){
    return 1
  }else{
    return n * fn(n - 1)
  }
}

# 最后提一嘴

function fn(obj){
  const {name, age, gender ,hobby} = obj
  console.log(name)
  console.log(age)
  console.log(gender)
  console.log(hobby)
}
let p = {
  name:'zhangsan',
  age:12,
  gender:'male',
  hobby:'王者荣耀'
}
fn(p)

当我们的函数中需要传入的参数较多的时候(大于3个的时候),我们可以将参数变成一个对象,然后通过对象的解构赋值来获取每个参数.这样我们就不用去操心每个参数的先后顺序了.

# 总结

在这里插入图片描述