ES6 深入:剩余参数和默认值

ES6 深入 系列文章介绍了 ECMAScript 标准第六版(简称 ES6)中为 JavaScript 编程语言添加的新特性。

今天的内容是关于两个使 JavaScript 函数语法更具表现力的特性:剩余参数和参数默认值。

剩余参数

在创建 API 时,一个常见的需求是 *可变参数函数*,即接受任意数量参数的函数。例如,String.prototype.concat 方法可以接受任意数量的字符串参数。ES6 通过剩余参数提供了编写可变参数函数的新方法。

为了演示,让我们编写一个简单的可变参数函数 containsAll,该函数检查一个字符串是否包含多个子字符串。例如,containsAll("banana", "b", "nan") 将返回 true,而 containsAll("banana", "c", "nan") 将返回 false

这是实现该函数的传统方式

function containsAll(haystack) {
  for (var i = 1; i < arguments.length; i++) {
    var needle = arguments[i];
    if (haystack.indexOf(needle) === -1) {
      return false;
    }
  }
  return true;
}

此实现使用了神奇的 arguments 对象,它是一个类似数组的对象,包含传递给函数的参数。这段代码确实可以完成我们想要的功能,但可读性并不理想。函数参数列表仅包含一个参数 haystack,因此无法一目了然地知道该函数实际上接受多个参数。此外,我们必须注意从索引 1 而不是 0 开始遍历 arguments,因为 arguments[0] 对应于 haystack 参数。如果我们想在 haystack 之前或之后添加另一个参数,我们必须记住更新 for 循环。剩余参数解决了这两个问题。以下是使用剩余参数的 ES6 实现 containsAll 的自然方式

function containsAll(haystack, ...needles) {
  for (var needle of needles) {
    if (haystack.indexOf(needle) === -1) {
      return false;
    }
  }
  return true;
}

这个版本的函数与第一个版本的行为相同,但包含了特殊的 ...needles 语法。让我们看看调用此函数的工作原理,以调用 containsAll("banana", "b", "nan") 为例。参数 haystack 像往常一样用第一个传递的参数填充,即 "banana"needles 前面的省略号表示它是一个 *剩余参数*。所有其他传递的参数将被放入一个数组并分配给变量 needles。对于我们示例调用,needles 被设置为 ["b", "nan"]。然后,函数执行像往常一样继续。(请注意,我们使用了 ES6 的 for-of 循环结构。)

函数中只有最后一个参数可以标记为剩余参数。在调用中,剩余参数之前的参数将像往常一样填充。任何“额外的”参数将被放入一个数组并分配给剩余参数。如果没有额外的参数,剩余参数将只是一个空数组;剩余参数永远不会是 undefined

默认参数

通常,函数不需要所有可能的参数都由调用者传递,并且对于未传递的参数,可以使用合理的默认值。JavaScript 一直以来都有一个不灵活的默认参数形式;对于没有传递值的参数,默认为 undefined。ES6 引入了一种指定任意参数默认值的方法。

下面是一个示例。(反引号表示模板字符串,它们在 上周讨论过。)

function animalSentence(animals2="tigers", animals3="bears") {
    return `Lions and ${animals2} and ${animals3}! Oh my!`;
}

对于每个参数,= 后面的部分是一个表达式,指定了如果调用者没有传递该参数,则该参数的默认值。因此,animalSentence() 返回 "Lions and tigers and bears! Oh my!"animalSentence("elephants") 返回 "Lions and elephants and bears! Oh my!",而 animalSentence("elephants", "whales") 返回 "Lions and elephants and whales! Oh my!"

与默认参数相关联的几个微妙之处

  • 与 Python 不同,**默认值表达式在函数调用时从左到右进行评估**。这也意味着默认表达式可以使用先前填充的参数的值。例如,我们可以将我们的动物句子函数变得更高级,如下所示
    function animalSentenceFancy(animals2="tigers",
        animals3=(animals2 == "bears") ? "sealions" : "bears")
    {
      return `Lions and ${animals2} and ${animals3}! Oh my!`;
    }
    

    然后,animalSentenceFancy("bears") 返回 "Lions and bears and sealions. Oh my!"

  • 传递 undefined 被认为等同于不传递任何内容。因此,animalSentence(undefined, "unicorns") 返回 "Lions and tigers and unicorns! Oh my!"
  • 没有默认值的参数隐式默认为 undefined,因此
    function myFunc(a=42, b) {...}
    

    是允许的,等同于

    function myFunc(a=42, b=undefined) {...}
    

关闭 arguments

我们现在已经看到,剩余参数和默认值可以替代 arguments 对象的使用,而移除 arguments 通常会使代码更易于阅读。除了影响可读性之外,arguments 对象的神奇之处还会臭名昭著地导致 优化 JavaScript VM 的头痛问题.

希望剩余参数和默认值可以完全取代 arguments。作为迈向这一目标的第一步,使用剩余参数或默认值的函数被禁止使用 arguments 对象。对 arguments 的支持不会很快消失,即使永远不会消失,但现在最好在可能的情况下使用剩余参数和默认值来避免使用 arguments

浏览器支持

Firefox 从 15 版本开始支持剩余参数和默认值。

不幸的是,其他已发布的浏览器尚未支持剩余参数或默认值。V8 最近 添加了对剩余参数的实验性支持,并且有一个开放的 V8 问题用于实现默认值。JSC 也有关于 剩余参数默认值 的开放问题。

BabelTraceur 编译器都支持默认参数,因此您可以从今天开始使用它们。

结论

虽然在技术上不允许任何新的行为,但剩余参数和参数默认值可以使某些 JavaScript 函数声明更具表现力和可读性。祝您调用愉快!


注意:感谢 Benjamin Peterson 在 Firefox 中实现了这些功能,感谢他对该项目的所有贡献,当然还要感谢本周的帖子。

下周,我们将介绍另一个简单、优雅、实用、日常的 ES6 功能。它利用您已经用来编写数组和对象的熟悉语法,并将其颠倒过来,产生了一种新的、简洁的方法来 *分解数组和对象*。这意味着什么?为什么要分解对象?下周四加入我们,了解 Mozilla 工程师 Nick Fitzgerald 对 ES6 解构的深入介绍。

Jason Orendorff

ES6 深入 编辑

关于 Benjamin Peterson

Benjamin Peterson 的更多文章…


9 条评论

  1. Oz

    我认为默认表达式可以是函数调用,因此

    function setDiscount(product, discount=getDefaultDiscountFor(product))

    将是有效的,甚至可能是内联函数?

    function setDiscount(saleprice, discount=(function(saleprice) {
    if (saleprice > 100) return 10;
    if (saleprice > 50) return 2.5;
    return 0;
    })(saleprice)) {
    console.log(discount);
    }

    2015 年 5 月 22 日 03:57

    1. Benjamin Peterson

      没错。这两个都应该可以工作,即使第二个不产生最漂亮的代码。:)

      2015 年 5 月 22 日 08:41

  2. Colin P. Hill

    默认值很棒,了解它们的语义也很重要,但我一定会掐死任何我在参数列表中放置逻辑的人。

    2015 年 5 月 22 日 18:05

  3. simonleung

    如果剩余参数中的空数组是未定义的而不是一个没有元素的数组,这通常没有用。

    此外,我们不能同时使用剩余参数和默认值。例如:
    function fun(…arr=[1,2]){} 将会产生语法错误。
    这没有意义吗?

    2015 年 5 月 23 日 下午 06:11

    1. Benjamin Peterson

      如果剩余参数始终是数组,这被认为更一致。例如,这样你就不必在尝试遍历它之前检查参数是否未定义。

      2015 年 5 月 23 日 下午 11:41

  4. 拉尔夫

    所有参数是在应用任何默认值之前分配,还是每个参数都是从左到右设置的。

    示例
    function animalSentence(animals2=animals3,
    animals3=”bears”)


    {
    return `Lions and ${animals2} and ${animals3}! Oh my!`;
    }

    animalSentence(undefined, “turtles”);

    animals2 将是 “turtles”,未定义,还是可能是 “bears”?

    2015 年 5 月 26 日 上午 07:43

    1. Benjamin Peterson

      给定的参数将分配给参数名称,然后评估默认值。因此,在此示例中,animals2 将是 "turtles"

      2015 年 5 月 26 日 上午 09:50

  5. 马泰·谢普尔

    我想,你指的是 containsAll 应该读作:

    function containsAll(haystack, …needles) {
    return needles.every(x => haystack.indexOf(x) !== 0);
    }

    对吗?

    2015 年 5 月 29 日 上午 01:53

  6. Benjamin Peterson

    是的,every() 也能工作,但我想要演示 for-of 循环。

    2015 年 5 月 29 日 上午 08:11

这篇文章的评论已关闭。