JavaScript 的函数比较特殊的是可以接受任意个数的参数,完全无视函数声明中的参数个数。

ES5 默认参数模拟

在 ES5 或更早的版本中,可以通过以下方式来给参数添加默认值

function makeRequest (url, timeout, callback) {
  timeout = timeout || 2000;
  callback = callback || function () {};
  // ...
}

上面的方法有个缺点,如果给 timeout 传入的值是 0 ,那么 timeout 的值会被替换为 2000,因为 0 视为假。


下面的例子会对参数进行类型检查

function makeRequest (url, timeout, callback) {
  timeout = (typeof timeout !== 'undefined') ? timeout : 2000;
  callback = (typeof callback !== 'undefined') ? callback : function () {};
}

ES6 中的默认参数

ES6 中函数参数能更方便地获取默认值,当未传入实参给形参时,形参的默认值会被使用

function makeRequest (url, timeout = 2000, callback = function () {}) {
  // ...
}

该函数只期待传入一个参数,其余 2 个参数有各自的默认值。这使得函数体更加小巧。因为不再需要添加额外的代码来检查是否有遗漏的参数值。

如果使用 3 个参数来调用 makeRequest() ,那么所有的默认值都不会被使用。

makeRequest('/foo');  // timeout 和 callback 使用默认值
makeRequest('/foo', 500);  // callback 使用默认值
makeRequest('/foo', 500, function (body) {  // 没有使用任何默认值
  doSomething(body);
});

ES6 认为 url 参数是必须传入的,这就是 3 次调用都传入 /foo 的原因。其余 2 个参数被视为是可选的。


另外,也可以任意指定其中一个函数参数的默认值,如果之后的参数未设定默认值也是可以的。

function makeRequest (url, timeout = 2000, callback) {
  // ...
}

timeout 的默认值只在未提供默认值或者传入 undefined 的情况下才会被使用

makeRequest('/foo', undefined, function (body) {  // 使用 timeout 的默认值
  doSomething(body);
});

makeRquest('/foo');  // 使用 timeout 的默认值

makeRequest('/foo', null, function (body) {  // 使用 null
  doSomething(body);
});

在提供 null 作为参数值的时候是有效的。

默认参数对 arguments 对象的影响

需要记住的是当使用默认参数的时候,arguments 对象的表现是不同的。

在 ES5 的非严格模式中,arguments 对象会反映出所有被命名的参数的变化。

function mixArgs (first, second) {
  console.log(first === arguments[0]);  // true
  console.log(second === arguments[1]);  // true
  first = 'c';
  second = 'd';
  console.log(first === arguemnts[0]);  // true
  console.log(second === arguments[1]);  // true
}

mixArgs('a', 'b');

可以看到,arguments 对象在非严格模式下是实时更新命名参数的变化。

但是,在 ES5 的严格模式下,这个机制被取消了。arguments 对象不会反映任何命名参数。

function mixArgs (first, second) {
  "use strict"
  console.log(first === arguments[0]);  // true
  console.log(second === arguments[1]);  // true
  first = 'c';
  second = 'd';
  console.log(first === arguemnts[0]);  // false
  console.log(second === arguments[1]);  // false
}

mixArgs('a', 'b');

上面的 firstsecond 的更新不会映射到 arguments 内。


当使用 ES6 的默认参数时,arguments 对象的表现和 ES5 的严格模式一致 (不管是否设定为严格模式)。

默认参数的存在会使 arguments 对象对该命名参数解绑。

这是个很重要的细节!因为 arguments 对象的使用方式发生了变化。

// 非严格模式
function mixArgs (first, second = 'b') {
  console.log(arguments.length);  // 1
  console.log(first === arguments[0]);  // true
  console.log(second === arguments[1]);  // false
  first = 'c';
  second = 'd';
  console.log(first === arguments[0]);  // false
  console.log(second === arguments[1]);  // false
}

mixArgs('a');

arguments.length 为 1 是因为只给 mixArgs() 只提供了一个参数。

这说明 arguments[1] 的值是 undefined,同时 first 等于 arguments[0]

改变 firstsecond 的值不会对 arguments 造成任何效果,不论是在严格模式还是在非严格模式。

所以,arguments 总是反映出函数的首次调用状态。


默认参数表达式

默认参数其中一个特性就是传给它的不一定是原始值。例如,你可以执行一个函数并把返回值作为参数。

function getValue () {
  return 10;
}
function add (first, second = getValue()) {
  return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(1));  // 11

这里,需要注意的是,getValue() 只会在未提供实参给 second 的情况下才会被调用。

let value = 5;
function getValue () {
  return value++;
}
function add (first, second = getValue()) {
  return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(1));  // 6
console.log(add(1));  // 7

在这个例子中,value 的初始值是 5 并随着每次 getValue() 的调用而递增。

首次调用 add(1) 返回的值是 6,再次调用则返回 7,因为 value 的值已经增加了。 由于 second 的默认值总是在当前 add 函数被调用的情况下才被计算,所以 value 的值可以随时被改变。

使用函数调用作为默认值的时候需要注意,如果你忘了在函数名后面添加括号,例如如果把上例中的 second = getValue() 写成 second = getValue, 那么,你传入的是一个函数引用而不是函数调用后返回的值。

另外,如果把前面的参数作为后面的默认值。

function add (first, second = first) {
  return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(1));  // 2

first 作为默认值提供给 second 参数,这意味着只传入一个参数时,2 个参数获得了相同的值。

再进一步,你可以把 first 作为参数传给另一个函数以便计算返回值传给 second

function getValue (value) {
  return value + 5;
}
function add (first, second = getValue(first)) {
  return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(1));  // 7

另外,默认参数引用其他参数的场景只发生在引用之前的参数,即前面的参数不能访问后面的参数。例如:

function add (first = second, second) {
  return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(undefined, 1));  // 抛出错误

调用 add(undefined, 1) 发生错误是因为 second 是在 first 之后定义,所以 first 无法访问 second 的值。

要想知道缘由,就要重温一重要概念:暂存性死区。


默认参数的暂存性死区

let 声明类似,每个参数都创建了一个新的绑定,但是在它们被初始化之前访问会抛出错误。 初始化的方式是通过在函数被调用的时候传递参数或者使用默认参数。

function getValue (value) {
  return value + 5;
}
function add (first, second = getValue(first)) {
  return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(1));  // 7

调用 add(1, 1)add(1) 事实上执行了以下语句以创建 firstsecond 参数和赋值:

// 调用 add(1, 1)
let first = 1;
let second = 1;

// 调用 add(1)
let first = 1;
let second = getValue(first);

当函数 add() 执行时,firstsecond 的绑定被移入了特定的参数 TDZ (类似 let)。 之所以 second 可以被 first 初始化是因为 first 的初始化在前,反之则不能。

function add (first = second, second) {
  return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(undefined, 1));  // 抛出错误

在这个例子中,调用 add(1, 1)add(undefined, 1) 实际上做了以下工作:

// 调用 add(1, 1)
let first = 1;
let second = 1;

// 调用 add(undefined, 1)
let first = second;
let second = 1;

调用 add(undefined, 1) 时出现错误是因为在 let first = second 的时候,second 还没初始化。 这个时候,second 仍在 TDZ 中,所以访问它会出错。这些与之前提到过的 let 绑定十分相似。


总结:函数参数相比函数内部有着自己的作用域和 TDZ,这意味着参数的默认值不能使用函数内部声明的任何变量。