函数参数的默认值
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
上面代码检查函数 log 的参数 y 有没有赋值,如果没有,则指定默认值为 World。这种写法的缺点在于,如果参数 y 赋值了,但是对应的布尔值为 false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数 y 是否被赋值,如果没有,再等于默认值。
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。下面是另一个例子。
除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
参数变量是默认声明的,所以不能用 let 或 const 再次声明。
上面代码中,参数变量x是默认声明的,在函数体中,不能用 let 或 const 再次声明,否则会报错。
使用参数默认值时,函数不能有同名参数。
另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
上面代码中,参数 p 的默认值是 x + 1。这时,每次调用函数 foo,都会重新计算 x + 1,而不是默认 p 等于 100。
与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值,结合起来使用。
上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数 foo 的参数是一个对象时,变量 x 和 y 才会通过解构赋值生成。如果函数 foo 调用时没提供参数,变量 x 和 y 就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。
上面代码指定,如果没有提供参数,函数 foo 的参数默认为一个空对象。
下面是另一个解构赋值默认值的例子。
上面代码中,如果函数 fetch 的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
上面代码中,函数 fetch 没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量 method 才会取到默认值 GET。
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入 undefined。
如果传入 undefined,将触发该参数等于默认值,null 则没有这个效果。
上面代码中,x 参数对应 undefined,结果触发了默认值,y 参数等于 null,就没有触发默认值。
函数的 length 属性
指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length 属性将失真。
上面代码中,length 属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数c指定了默认值,因此 length 属性等于 3 减去 1,最后得到 2。
这是因为 length 属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入 length 属性。
如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数了。
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
上面代码中,参数 y 的默认值等于变量 x。调用函数 f 时,参数形成一个单独的作用域。在这个作用域里面,默认值变量 x 指向第一个参数 x,而不是全局变量 x,所以输出是 2。
再看下面的例子。
上面代码中,函数 f 调用时,参数 y = x 形成一个单独的作用域。这个作用域里面,变量 x 本身没有定义,所以指向外层的全局变量 x。函数调用时,函数体内部的局部变量 x 影响不到默认值变量x。
如果此时,全局变量 x 不存在,就会报错。
下面这样写,也会报错。
上面代码中,参数 x = x 形成一个单独作用域。实际执行的是 let x = x,由于暂时性死区的原因,这行代码会报错 “x 未定义”。
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
上面代码中,函数 bar 的参数 func 的默认值是一个匿名函数,返回值为变量 foo。函数参数形成的单独作用域里面,并没有定义变量 foo,所以 foo 指向外层的全局变量 foo,因此输出 outer。
如果写成下面这样,就会报错。
上面代码中,匿名函数里面的 foo 指向函数外层,但是函数外层并没有声明变量 foo,所以就报错了。
下面是一个更复杂的例子。
上面代码中,函数 foo 的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量 y,y 的默认值是一个匿名函数。这个匿名函数内部的变量 x,指向同一个作用域的第一个参数 x。函数 foo 内部又声明了一个内部变量 x,该变量与第一个参数 x 由于不是同一个作用域,所以不是同一个变量,因此执行 y 后,内部变量 x 和外部全局变量 x 的值都没变。
如果将 var x = 3 的 var 去除,函数 foo 的内部变量 x 就指向第一个参数 x,与匿名函数内部的 x 是一致的,所以最后输出的就是 2,而外层的全局变量 x 依然不受影响。