默认参数值
也许在JavaScript中最常见的惯用法之一就是为函数参数设置默认值。我们多年来一直使用的方法应当看起来很熟悉:
function foo(x,y) {
x = x || 11;
y = y || 31;
console.log( x + y );
}
foo(); // 42
foo( 5, 6 ); // 11
foo( 5 ); // 36
foo( null, 6 ); // 17
当然,如果你曾经用过这种模式,你就会知道它既有用又有点儿危险,例如如果你需要能够为其中一个参数传入一个可能被认为是falsy的值。考虑下面的代码:
foo( 0, 42 ); // 53 <-- 噢,不是42
为什么?因为0
是falsy,因此x || 11
的结果为11
,而不是直接被传入的0
。
为了填这个坑,一些人会像这样更加啰嗦地编写检查:
function foo(x,y) {
x = (x !== undefined) ? x : 11;
y = (y !== undefined) ? y : 31;
console.log( x + y );
}
foo( 0, 42 ); // 42
foo( undefined, 6 ); // 17
当然,这意味着除了undefined
以外的任何值都可以直接传入。然而,undefined
将被假定是这样一种信号,“我没有传入这个值。” 除非你实际需要能够传入undefined
,它就工作的很好。
在那样的情况下,你可以通过测试参数值是否没有出现在arguments
数组中,来看它是否实际上被省略了,也许是像这样:
function foo(x,y) {
x = (0 in arguments) ? x : 11;
y = (1 in arguments) ? y : 31;
console.log( x + y );
}
foo( 5 ); // 36
foo( 5, undefined ); // NaN
但是在没有能力传入意味着“我省略了这个参数值”的任何种类的值(连undefined
也不行)的情况下,你如何才能省略第一个参数值x
呢?
foo(,5)
很诱人,但它不是合法的语法。foo.apply(null,[,5])
看起来应该可以实现这个技巧,但是apply(..)
的奇怪之处意味着这组参数值将被视为[undefined,5]
,显然它没有被省略。
如果你深入调查下去,你将发现你只能通过简单地传入比“期望的”参数值个数少的参数值来省略末尾的参数值,但是你不能省略在参数值列表中间或者开头的参数值。这就是不可能。
这里有一个施用于JavaScript设计的重要原则需要记住:undefined
意味着 缺失。也就是,在undefined
和 缺失 之间没有区别,至少是就函数参数值而言。
注意: 容易令人糊涂的是,JS中有其他的地方不适用这种特殊的设计原则,比如带有空值槽的数组。更多信息参见本系列的 类型与文法。
带着所有这些认识,现在我们可以检视在ES6中新增的一种有用的好语法,来简化对丢失的参数值进行默认值的赋值。
function foo(x = 11, y = 31) {
console.log( x + y );
}
foo(); // 42
foo( 5, 6 ); // 11
foo( 0, 42 ); // 42
foo( 5 ); // 36
foo( 5, undefined ); // 36 <-- `undefined`是缺失
foo( 5, null ); // 5 <-- null强制转换为`0`
foo( undefined, 6 ); // 17 <-- `undefined`是缺失
foo( null, 6 ); // 6 <-- null强制转换为`0`
注意这些结果,和它们如何暗示了与前面的方式的微妙区别和相似之处。
与常见得多的x || 11
惯用法相比,在一个函数声明中的x = 11
更像x !== undefined ? x : 11
,所以在将你的前ES6代码转换为这种ES6默认参数值语法时要多加小心。
注意: 一个剩余/收集参数(参见“扩散/剩余”)不能拥有默认值。所以,虽然function foo(...vals=[1,2,3]) {
看起来是一种迷人的能力,但它不是合法的语法。有必要的话你需要继续手动实施那种逻辑。
默认值表达式
函数默认值可以比像31
这样的简单值复杂得多;它们可以是任何合法的表达式,甚至是函数调用:
function bar(val) {
console.log( "bar called!" );
return y + val;
}
function foo(x = y + 3, z = bar( x )) {
console.log( x, z );
}
var y = 5;
foo(); // "bar called"
// 8 13
foo( 10 ); // "bar called"
// 10 15
y = 6;
foo( undefined, 10 ); // 9 10
如你所见,默认值表达式是被懒惰地求值的,这意味着他们仅在被需要时运行 —— 也就是,当一个参数的参数值被省略或者为undefined
。
这是一个微妙的细节,但是在一个函数声明中的正式参数是在它们自己的作用域中的(将它想象为一个仅仅围绕在函数声明的(..)
外面的一个作用域气泡),不是在函数体的作用域中。这意味着在一个默认值表达式中的标识符引用会在首先在正式参数的作用域中查找标识符,然后再查找一个外部作用域。更多信息参见本系列的 作用域与闭包。
考虑如下代码:
var w = 1, z = 2;
function foo( x = w + 1, y = x + 1, z = z + 1 ) {
console.log( x, y, z );
}
foo(); // ReferenceError
在默认值表达式w + 1
中的w
在正式参数作用域中查找w
,但没有找到,所以外部作用域的w
被使用了。接下来,在默认值表达式x + 1
中的x
在正式参数的作用域中找到了x
,而且走运的是x
已经被初始化了,所以对y
的赋值工作的很好。
然而,z + 1
中的z
找到了一个在那个时刻还没有被初始化的参数变量z
,所以它绝不会试着在外部作用域中寻找z
。
正如我们在本章早先的“let
声明”一节中提到过的那样,ES6拥有一个TDZ,它会防止一个变量在它还没有被初始化的状态下被访问。因此,z + 1
默认值表达式抛出一个TDZReferenceError
错误。
虽然对于代码的清晰度来说不见得是一个好主意,一个默认值表达式甚至可以是一个内联的函数表达式调用 —— 通常被称为一个立即被调用的函数表达式(IIFE):
function foo( x =
(function(v){ return v + 11; })( 31 )
) {
console.log( x );
}
foo(); // 42
一个IIFE(或者任何其他被执行的内联函数表达式)作为默认值表示来说很合适是非常少见的。如果你发现自己试图这么做,那么就退一步再考虑一下!
警告: 如果一个IIFE试图访问标识符x
,而且还没有声明自己的x
,那么这也将是一个TDZ错误,就像我们刚才讨论的一样。
前一个代码段的默认值表达式是一个IIFE,这是因为它是通过(31)
在内联时立即被执行。如果我们去掉这一部分,赋予x
的默认值将会仅仅是一个函数的引用,也许像一个默认的回调。可能有一些情况这种模式将十分有用,比如:
function ajax(url, cb = function(){}) {
// ..
}
ajax( "http://some.url.1" );
这种情况下,我们实质上想在没有其他值被指定时,让默认的cb
是一个没有操作的空函数。这个函数表达式只是一个函数引用,不是一个调用它自己(在它末尾没有调用的()
)以达成自己目的的函数。
从JS的早些年开始,就有一个少为人知但是十分有用的奇怪之处可供我们使用:Function.prototype
本身就是一个没有操作的空函数。这样,这个声明可以是cb = Function.prototype
而省去内联函数表达式的创建。