通常来说,函数调用者不需要传递所有可能存在的参数,没有被传递的参数可由感知到的默认参数进行填充。JavaScript 有严格的默认参数格式,未被传值的参数默认为 undefined
。ES6 引入了一种新方式,可以指定任意参数的默认值。
JavaScript 函数参数的默认值都是undefined
, ES5 里,不支持直接在形参里写默认值。所以,要设置默认值,就要检测参数是否为undefined
,按需求赋值。
function fn(x, y) {y = y || 'World';console.log(x, y);}fn('Hello');// Hello Worldfn('Hello', 'China');// Hello Chinafn('Hello', '');// Hello World
缺点:如果参数 y
赋值了,但是对应的布尔值为 false
,则该赋值不起作用。
为了避免这个问题,我们需要先判断参数 y
是否被赋值,如果没有,再等于默认值。
function fn(x, y) {y = typeof y === undefined ? y || 'World';console.log(x, y);}
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function fn(x, y = 'World') {console.log(x, y);}log('Hello');// Hello Worldlog('Hello', 'China');// Hello Chinalog('Hello', '');// Hello
优点:
参数变量是 默认声明 的,所以不能用 let
或 const
再次声明。
function fn(x = 1) {let x = 2;// SyntaxError: Identifier 'x' has already been declaredconst x = 3;// SyntaxError: Identifier 'x' has already been declared}
使用参数默认值时,函数不能有同名参数。
// 不报错function fn(x, x, y) {// do something}// 报错function fn(x, x, y = 1) {// do something}// SyntaxError: Duplicate parameter name not allowed in this context
参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
let x = 99;function fn(p = x + 1) {console.log(p);}fn();// 100x = 100;fn();// 101
function fn({ x, y = 5 }) {console.log(x, y);}fn({});// undefined 5fn({ x: 1 });// 1 5fn({ x: 1, y: 2 });// 1 2fn();// TypeError: Cannot read property 'x' of undefined
上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数 fn
的参数是一个对象时,变量 x
和 y
才会通过解构赋值生成。如果函数 fn
调用时没提供参数,变量 x
和 y
就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。
function fn({ x, y = 5 } = {}) {console.log(x, y);}fn();// undefined 5
下面是另一个解构赋值默认值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {console.log(method);}fetch('http://example.com', {});// "GET"fetch('http://example.com');// VM1292:1 Uncaught TypeError: Cannot read property 'body' of undefined// at fetch (<anonymous>:1:23)// at <anonymous>:5:1
上面代码中,如果函数 fetch
的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {console.log(method);}fetch('http://example.com');// "GET"
上面代码中,函数 fetch
没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量 method
才会取到默认值 GET
。
// 写法一function fn1({ x = 0, y = 0 } = {}) {return [x, y];}// 写法二function fn2({ x, y } = { x: 0, y: 0 }) {return [x, y];}
上面两种写法都对函数的参数设定了默认值,区别是:
// 函数没有参数的情况fn1();// [0, 0]fn2();// [0, 0]// x 和 y 都有值的情况fn1({ x: 3, y: 8 });// [3, 8]fn2({ x: 3, y: 8 });// [3, 8]// x 有值,y 无值的情况fn1({ x: 3 });// [3, 0]fn2({ x: 3 });// [3, undefined]// x 和 y 都无值的情况fn1({});// [0, 0];fn2({});// [undefined, undefined]fn1({ z: 3 });// [0, 0]fn2({ z: 3 });// [undefined, undefined]
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// example 1function fn(x = 1, y){return [x, y];}fn();// [1, undefined]fn(2);// [2, undefined]fn(, 1);// Uncaught SyntaxError: Unexpected token ,fn(undefined, 1);// [1, 1]// example 2function bar(x, y = 5, z){return [x, y, z];}bar();// [undefined, 5, undefined]bar(1);// [1, 5, undefined]bar(1, ,2);// Uncaught SyntaxError: Unexpected token ,bar(1, undefined, 2);// [1, 5, 2]
如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
function fn(x = 5, y = 6) {console.log(x, y);}fn(undefined, null);// 5 null
指定了默认值以后,函数的 length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
示例一:一个参数,没有默认值
(function(a) {}.length);// 1
示例二:一个参数,有默认值
(function(a = 5) {}.length);// 0
示例三:三个参数,其中一个参数有默认值
(function(a, b, c = 5) {}.length);// 2
上面代码中,length
属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数 c
指定了默认值,因此 length
属性等于 3
减去 1
,最后得到 2
。
这是因为 length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入 length
属性。
(function(...args) {}.length); // 0
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了。
(function(a = 0, b, c) {}.length(// 0function(a, b = 1, c) {}).length);// 1
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;function fn(x, y = x) {console.log(y);}f(2); // 2
上面代码中,参数 y
的默认值等于变量 x
。调用函数 fn
时,参数形成一个单独的作用域。在这个作用域里面,默认值变量 x
指向第一个参数 x
,而不是全局变量 x
,所以输出是 2
。
再看下面的例子。
let x = 1;function fn(y = x) {let x = 2;console.log(y);}fn(); // 1
上面代码中,函数 fn
调用时,参数 y = x
形成一个单独的作用域。这个作用域里面,变量 x
本身没有定义,所以指向外层的全局变量 x
。函数调用时,函数体内部的局部变量 x
影响不到默认值变量 x
。
如果此时,全局变量 x
不存在,就会报错。
function fn(y = x) {let x = 2;console.log(y);}fn(); // ReferenceError: x is not defined
下面这样写,也会报错。
var x = 1;function fn(x = x) {// ...}fn(); // ReferenceError: x is not defined
上面代码中,参数 x = x
形成一个单独作用域。实际执行的是 let x = x
,由于暂时性死区的原因,这行代码会报错 x is not defined
(指第二个 x
未定义)。
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
let fn = 'outer';function bar(func = () => fn) {let fn = 'inner';console.log(func());}bar(); // outer
上面代码中,函数 bar
的参数 func
的默认值是一个匿名函数,返回值为变量 fn
。函数参数形成的单独作用域里面,并没有定义变量 fn
,所以 fn
指向外层的全局变量 fn
,因此输出 outer
。
如果写成下面这样,就会报错。
function bar(func = () => fn) {let fn = 'inner';console.log(func());}bar(); // ReferenceError: fn is not defined
上面代码中,匿名函数里面的 fn
指向函数外层,但是函数外层并没有声明变量 fn
,所以就报错了。
下面是一个更复杂的例子。
var x = 1;function fn(x,y = function() {x = 2;}) {var x = 3;y();console.log(x);}fn(); // 3x; // 1
上面代码中,函数 fn
的参数形成一个单独作用域。这个作用域里面,首先声明了变量 x
,然后声明了变量y
,y
的默认值是一个匿名函数。这个匿名函数内部的变量 x
,指向同一个作用域的第一个参数 x
。函数 fn
内部又声明了一个内部变量 x
,该变量与第一个参数 x
由于不是同一个作用域,所以不是同一个变量,因此执行y
后,内部变量x
和外部全局变量 x
的值都没变。
如果将 var x = 3
的 var
去除,函数 fn
的内部变量 x
就指向第一个参数 x
,与匿名函数内部的 x
是一致的,所以最后输出的就是 2
,而外层的全局变量 x
依然不受影响。
var x = 1;function fn(x,y = function() {x = 2;}) {x = 3;y();console.log(x);}fn();// 2console.log(x);// 1
总结:
现在我们已经看到了 arguments
对象可被不定参数和默认参数完美代替,移除 arguments
后通常会使代码更易于阅读。除了破坏可读性外,众所周知,针对 arguments
对象对 JavaScript 虚拟机进行的优化会导致一些让你头疼不已的问题。
我们期待着不定参数和默认参数可以完全取代 arguments
,要实现这个目标,标准中增加了相应的限制:在使用不定参数或默认参数的函数中禁止使用 arguments
对象。曾经实现过 arguments
的引擎不会立即移除对它的支持,当然,现在更推荐使用不定参数和默认参数。