专栏首页京程一灯面向 JavaScript 开发人员的 ECMAScript 6 指南(2):ECMAScript 6 中的函数增强

面向 JavaScript 开发人员的 ECMAScript 6 指南(2):ECMAScript 6 中的函数增强

第 2 部分专门介绍将改变您使用 JavaScript 中的函数的方式的语言更新。在这一部分中,我们将介绍函数定义和调用语法,您将进一步了解解构赋值(这一次在函数定义中)。我还将介绍新的箭头函数语法和生成器函数,后者给经典的迭代器和古老的for 循环带来了有趣的转机。

函数声明中的解构

JavaScript 的新解构赋值得名于数组或对象可以 “解构” 并提取出组成部分的概念。在 第 1 部分 中,我们学习了如何在局部变量中使用解构。它在函数参数声明中也很有用。 如果某个函数需要一个对象,您可以在函数开始之前,利用解构功能提取出该对象的相关部分。可通过向函数的参数添加解构语法来实现此目的,如清单 1 所示。

清单 1. 函数声明中的解构

let person = {      firstName: "Ted",      lastName: "Neward",      age: 45,      favoriteLanguages: [        "ECMAScript", "Java", "C#", "Scala", "F#"       ]    }    function displayDetails({firstName, age}) {      console.log(`${firstName} is ${age} years old`);    }    displayDetails(person);

displayDetails 函数仅关心传入的对象的 firstName 和 age 字段。向对象语法添加一个解构赋值,可在函数调用中有效地提取出这些值。调用语法本身保持不变,这使得重构遗留代码来使用新语法变得更容易。

也可以在函数中使用解构数组语法,但它没有您即将看到的其他一些功能那么令人印象深刻。

函数参数

ECMAScript 对函数参数执行了一些语法改动。具体地讲,它为函数调用引入了默认参数值、剩余参数和展开运算符。我之前已经提到过,大部分更改都是 ECMAScript 开发人员已使用多年的约定之上的语法糖(syntactic sugar)。现在它们是成熟的语言功能,您可以使用更少的代码完成相同的工作。

我们首先将介绍默认参数,这可能是 3 个概念中最容易理解的概念。

默认参数

除了允许使用解构语法之外,ECMAScript 6 现在还提供了与一些 C 族语言中类似的默认参数值语法。即使您之前从未看到过默认参数,也很容易理解它们:

清单 2. 默认参数

let sayHello = function(message = "Hello world!") {  console.log(message);}sayHello();          // prints "Hello world!"sayHello("Howdy!");  // prints "Howdy!"

基本上讲:如果在调用位置指定了一个参数,那么该参数将接受传递的值;如果未指定值,则会分配默认值。

与 第 1 部分 中介绍的一些更新一样,新的默认参数实质上就是语法糖。在以前的 ECMAScript 版本中,您可能这样编写它:

清单 3. 旧式默认参数

var sayHello = function(message) {  if (message === undefined) {    message = "Hello world!";  }  console.log(message);}sayHello();          // prints "Hello world!"sayHello("Howdy!");  // prints "Howdy!"

新语法更短且表达能力更强,而且标准化了许多程序员多年来执行的工作。

剩余参数(Rest parameters)

ECMAScript 库中的一种更常见的做法是,定义函数或方法来接受一个或多个固定参数,后跟一组通过用户定义方式细化或修改调用的可选参数。在过去,可以通过访问静默构建并传递给每个函数调用的内置 arguments 参数来实现此目的:

清单 4. 旧式可选参数

function greet(firstName) {  var args = Array.prototype.slice.call(arguments, greet.length);  console.log("Hello",firstName);  if (args !== undefined)    args.forEach(function(arg) { console.log(arg); });}greet("Ted");greet("Ted", "Hope", "you", "like", "this!");

使用新的剩余参数语法,您可以将可选的参数捕获到一个局部数组变量中。然后按上面的相同方式使用它们,而无需执行修改:

清单 5. 剩余参数

function greet(firstName, ...args) {  console.log("Hello",firstName);  args.forEach(function(arg) { console.log(arg); });}greet("Ted");greet("Ted", "Hope", "you", "like", "this!");

请注意,剩余参数(第一个清单中的 args)不需要测试存在与否;该语言可确保它将以长度为 0 的数组形式存在,即使没有传递其他参数。

展开运算符

展开运算符(Spread operator)在某些方面与剩余参数的概念正好相反。剩余参数将会收集传入某个给定调用的一些可选值,展开运算符获取一个值数组并 “展开” 它们,基本上讲,就是解构它们以用作被调用的函数的各个参数。

展开运算符的最简单用例是将各个元素串联到一个数组中:

清单 6. 使用展开运算符进行串联

let arr1 = [0, 1, 2];let arr2 = [...arr1, 3, 4, 5];console.log(arr2); // prints 0,1,2,3,4,5

如果没有展开运算符语法,您需要提取第一个数组中的每个元素并附加到第二个数组,然后才添加剩余元素。

也可以在函数调用中使用展开运算符;事实上,这是您最有可能使用它的地方:

清单 7. 函数调用中的展开运算符

function printPerson(first, last, age) {  console.log(first, last age);}let args = ["Ted", "Neward", 45];printPerson(...args);

请注意,不同于剩余参数,展开运算符是在调用点上使用,而不是在函数定义中使用。

函数语法和语义

除了参数更改之外,ECMAScript 6 在函数语法和语义上也进行了重大改动。本节将介绍最重要的更新。只需记住,JavaScript 程序中的原始语法仍然可行。如果您最初感觉这种新语法不方便或不够直观,您可以逐步适应它的使用。

箭头函数

随着 Scala 和 F# 等新函数语言被大众接受,旧语言已开始采用它们的一些优秀功能。其中一项功能是箭头函数语法,这是一种用于创建函数字面量的速记符号。从 ECMAScript 6 开始,您可以使用所谓的粗箭头(与细箭头相对)创建函数字面量,就像这样:

清单 8. 创建函数字面量的箭头语法

let names = ["Ted","Jenni","Athen"];names.forEach((n) => console.log(n));

如果尝试过使用 C#、Java 8、Scala 或 F# 进行函数编程,您可能非常熟悉这种语法。即使您不熟悉它,箭头函数也很容易理解:箭头前的括号将参数捕获到函数主体,箭头本身表示函数主题的开头。如果主体仅包含一条语句或表达式,则不需要使用花括号。如果主体包含多条语句或表达式,那么可以通过在箭头后输入花括号来表示它们:

清单 9. 表示多条语句或表达式

let names = ["Ted","Jenni","Athen"];names.forEach((n) => {  console.log(n)});

如果只有一个参数,您可以选择完全省略括号,如下所示。(就个人而言,我甚至在只有一个参数时也使用括号,但这只是仁者见仁,智者见智的的审美偏好。)

清单 10. 单个参数

names.forEach(n => console.log(n));

箭头函数不能直接取代函数关键字。一般而言,您应该继续使用 function 定义方法(即与一个对象实例关联的函数)。为与对象无关的场景保留箭头函数,比如 Array.forEach 或 Array.map 调用的主体。因为箭头函数对待 this 的方式与普通函数不同,所以在方法定义中使用它们可能导致意料之外的结果。

另请注意,如果箭头函数的主体是只有一个值的单个表达式,则无需显式返回,而是应该将单一表达式隐式返回给箭头函数的调用方。但是,如果主体不只一条语句或表达式,则必须使用花括号,而且所有返回的值都必须通过常用的 “return” 语法发回给调用方。 ‘this’ 的新定义

在开始设计 ECMAScript 6 之前很长一段时间,程序员很难确定 ECMAScript 的 this 参数指向哪里。从表面上看,与其他 C 族语言一样,this 参数引用的对象上会调用一个方法,如下所示:

清单 11. ‘this’ 引用一个对象实例

let bob = {  firstName: "Bob",  lastName: "Robertson",  displayMe: function() {    for (let m in this) {      console.log(m,"=",this[m]);    }  }};bob.displayMe();

上面的参数显然引用了实例 bob,而且忠实地打印出 firstName、lastName 和 displayMe 方法(因为它也是该对象的成员)的名称和值。

当从一个存在于全局范围的函数引用 this 时,情况会变得有点怪异:

清单 12. ‘this’ 引用一个全局范围对象

let displayThis = function() {  for (let m in this) {    console.log(m);  }};displayThis();

对于缺乏经验的开发人员,ECMAScript 将全局范围定义为一个对象,所以当在全局范围内的函数使用时,this 引用全局范围对象,在上面的情况中,它忠实地打印出全局范围的每个成员,包括顶级全局变量、函数和对象(比如上面的示例中的 “console”)。

出于这个原因,我们也可以在两种不同的上下文中重用该函数,知道它每次将或多或少执行一些我们期望的操作:

清单 13. 重用全局范围函数

let displayThis = function() {  for (let m in this) {    console.log(m);  }};displayThis(); // this == global objectlet bob = {  firstName: "Bob",  lastName: "Robertson",  displayMe: displayThis};bob.displayMe(); // this == bob

可能此语法有点奇怪,但只要您理解了规则,就不是问题。直到您尝试使用 ECMAScript 构造函数作为对象类型时,情况才会真正偏离主题:

清单 14. 一个过度使用的 ‘this’

function Person() {  // The Person() constructor defines "this" as an instance   // of itself  this.age = 0;  setInterval(function growUp() {    // In non-strict mode, the growUp() function defines "this"     // as the global object; thus, "this.age" refers to a global    // "age" value, not the one defined on the instance of Person    this.age++;  }, 1000);}var p = new Person();// Every second, p.age is supposed to go up by one.// But because the "this" in the "growUp" function literal// refers to the global object, and not "p", p.age will// never change from 0.

词法 ‘this’ 绑定

为了解决与 this 相关的定义问题,箭头函数拥有所谓的词法 this 绑定。这意味着箭头函数在定义函数时使用 this 值,而不是在执行它时。

采用规则:完全理解新 this 规则可能需要一段时间。新箭头函数规则并不总是这么直观。作为开发人员,可以计划对 “内联” 函数使用箭头函数,对方法使用传统函数。如果这么做,各个方面都应按预期工作。

或许理解这一区别的最简单方法是借助一个旧的 Node.js 对象 EventEmitter。回想一下,EventEmitter(获取自 events 模块)是一个简单的发布-订阅式消息系统:您可以在某个特定事件名称上的发射器上注册回调,当该事件被 “发出” 时,则按注册的顺序触发回调。

如果向 EventEmitter 注册一个遗留函数,捕获的 this 将是在运行时确定的参数。但是如果您向 EventEmitter 注册一个箭头函数,this 将在定义箭头函数时绑定:

清单 15. ‘this’ 被箭头函数绑定

let EventEmitter = require('events');let ee = new EventEmitter();ee.on('event', function() {  console.log("function event fired", this);});ee.on('event', () => {  console.log("arrow event fired", this);});var bob = {  firstName: "Bob",  lastName: "Robertson"};bob.handleEventLegacy = function() {  console.log("function event fired", this);};bob.handleEventArrow = () => {  console.log("arrow event fired", this);};ee.on('event', bob.handleEventLegacy);ee.on('event', bob.handleEventArrow);ee.emit('event');

在触发函数事件时,this 被绑定到 EventEmitter 本身,而箭头事件未绑定到任何目标(它们分别打印一个空对象)。

生成器函数

生成器函数旨在生成一个值流供其他方使用。许多函数语言都使用了生成器,它们在其中可能名为流 或序列。现在 ECMAScript 中也引入了它们。

要了解生成器的实际工作原理,需要稍作解释。首先,想象一组简单的名称:

清单 16. 一个简单的集合

var names = ["Ted", "Charlotte", "Michael", "Matthew"];

现在假设我们希望一个函数返回每个名称,每个函数调用一次返回一个名称,直到全部返回:

清单 17. 一个返回每个名称的函数

var getName = (function() {  var current = 0;  return function() {    if (current > names.length)      return undefined;    else {      var temp = names[current];      current++;      return temp;    }  };})();console.log(getName()); // prints Tedconsole.log(getName()); // prints Charlotteconsole.log(getName()); // prints Michaelconsole.log(getName()); // prints Matthewconsole.log(getName()); // prints undefined

起初,上面的函数返回函数的方式可能看起来很陌生。这是必要的,因为 getName 函数需要在多个函数调用中跟踪它的状态。在类似 C 的语言中,可以将状态存储在 getName 函数内的静态变量中,但像类似的 Java 和 C# 一样,ECMAScript 不支持在函数中使用静态变量。在这种情况下,我们将使用闭包,以便函数字面量在返回后继续绑定到 “当前” 变量,使用该变量存储自己的状态。

要理解的重要一点是,此函数不会一次获取一个有限的值序列(采用返回数组的形式),它一次获取一个元素,直到没有剩余的元素。

但是如果要返回的元素永远用不完,该怎么办?

函数编程中的无限流

与在名称数组上使用迭代器相比,似乎前面的代码示例没有多大改进。毕竟,这就是迭代器的用途:让各个元素能够访问一个集合的内容。重要的变化表现在原始数组不再可见,甚至不再是数组时:

清单 18. 一个有限的流

var getName = (function() {  var current = 0;  return function() {    switch (current++) {      case 0: return "Ted";      case 1: return "Charlotte";      case 2: return "Michael";      case 3: return "Matthew";      default: return undefined;    }  };})();

从技术上讲,您看到的仍是一个迭代器,但它的实现看起来与来自样本的迭代器截然不同;这里没有集合,只有一组硬编码的值。

从本质上讲,它是一个没有关联集合的迭代器,这突出了一个重要的事实:我们的函数生成的值的来源 现在深度封装在离调用方很远的地方。这进而引入了一个更有趣的想法:调用方可能不知道最初没有集合,不知道生成的值永无止境。这就是一些语言所称的无限流。 斐波纳契数列(全球每种函数语言的 “Hello World” 等效程序)就是这样一个无限流:

清单 19. 斐波纳契数的无限流

var fibo = (function() {  var prev1 = undefined;  var prev2 = undefined;  return function() {    if (prev1 == undefined && prev2 == undefined) {      prev1 = 0;      return 0;    }    if (prev1 == 0 && prev2 == undefined) {      prev1 = 1;      prev2 = 0;      return 1;    }    else {      var ret = prev1 + prev2;      prev2 = prev1;      prev1 = ret;      return ret;    }  };})();

无限流 是一个从不会用完要返回的值的流。在这种情况下,斐波拉契数列没有逻辑终点。

JavaScript 中的反应式编程非常复杂。如果您打算了解更多的信息,可以访问 JavaScript 反应式编程 GitHub 页面。

尽管起初看起来很奇怪,但无限流的概念是其他一些基于 ECMAScript 的有趣技术(比如反应式编程)的核心。想想如果我们将用户事件(比如移动鼠标、单击按钮和按键)视为无限流,函数从流中获取每个事件并进行处理,结果会怎样?

构建无限流所需的代码量非常大,所以 ECMAScript 6 定义了一种新语法(和一个新关键字)来让代码更加简洁。在这里可以看到,我重写了清单 17 中的示例:

清单 20. 一个使用生成器的有限值流

function* getName() {  yield "Ted";  yield "Charlotte";  yield "Michael";  yield "Matthew";}let names = getName();console.log(names.next().value);console.log(names.next().value);console.log(names.next().value);console.log(names.next().value);console.log(names.next().value);

同样地,该函数将按顺序打印出每个名称。当它用完所有名称时,它会不停地打印 “undefined”。在语法上,yield 关键字看起来类似于 return,但事实上,它表示 “返回但记住我在此函数中的位置,以便下次调用它时,从离开的位置开始执行。”这显然比传统的 return 更复杂。

生成器的使用与第一个示例稍微不同:我们捕获了 getName 函数的返回值,然后像迭代器一样使用该对象。这是 ECMAScript 6 中的一个特意的设计决定。从技术上讲,生成器函数返回一个 Generator 对象,该对象用于从生成器函数获取各个值。新语法旨在尽可能地模拟迭代器。

谈到迭代器,还有最后一个需要知道的语法更改。

for-of 关键字

经典的 for 循环在 ECMAScript 6 具有了新形式,这是由于添加了一个辅助关键字:of。在许多方面,新语法与 for-in 没多大区别,但它支持生成器函数。

返回到清单 19 中的斐波纳契数列,这是向函数添加 for-of 关键字时发生的情况:

清单 21. 斐波纳契数列中的 ‘for of’

function* fibo() { // a generator function  yield 0;  yield 1;  let [prev, curr] = [0, 1];  while (true) {    [prev, curr] = [curr, prev + curr];    yield curr;  }}for (let n of fibonacci()) {  console.log(n);  // By the way, this is an infinite stream, so this loop  // will never terminate unless you break out of it}

for-of 和 for-in 之间存在着细微区别,但在大多数情况下,您可以使用 for-of 直接取代旧语法。它添加了隐式使用生成器的能力 — 就像我们在无限流示例中使用 getName() 执行的操作一样。

结束语

现在,您已注意到 ECMAScript 6 绝对不是一个简单的勘误版本。我们这里介绍的许多功能都源自函数编程概念,但不要被这种现象所欺骗:ECMAScript 6 远不止是一种函数语言。采用一些函数功能可以使编写 ECMAScript 代码变得更容易,但要实现此目的,您不需要知道单子 (monads)、独异点 (monoids) 和范畴论。在大多数情况下,流行的态度是:如果它适合您,则使用它;如果不适合,还有其他许多选择。

所以如果您从未执行过任何函数编程,不要紧张 — 您可以对 ECMAScript 6 浅尝辄止,绝不会深陷其中。

无论如何,本系列的下一篇文章又会让我们回到原点,介绍 ECMAScript 6 中新的基于类的语法和对象增强。

本文分享自微信公众号 - 京程一灯(jingchengyideng)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-08-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 提高 | 10个JavaScript难点

    立即执行函数,即Immediately Invoked Function Expression (IIFE),正如它的名字,就是创建函数的同时立即执行。它没有绑...

    疯狂的技术宅
  • JavaScript实现继承

    众所周知,JavaScript 这门语言在 ES6 出来之前是没有类(class)这一概念的,所以 JavaScript 中的类都是通过原型链来实现的。同样,使...

    疯狂的技术宅
  • 避免这些常见的JavaScript错误

    在今天,JavaScript是最流行的编程语言之一,如果你希望钻研JavaScript,这里有几个需要避免的问题

    疯狂的技术宅
  • 18Function类型

    函数是这样的一段 Javascript代码,它只定义一次,但可能被执行或调用多次。

    Dreamy.TZK
  • 快速掌握JavaScript面试基础知识(二)

    根据StackOverflow 调查, 自 2014 年一来,JavaScript 是最流行的编程语言。当然,这也在情理之中,毕竟 1/3 的开发工作都需要一些...

    Fundebug
  • javascript函数

    天天_哥
  • js基础-关于call,apply,bind的一切

    函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比...

    OBKoro1
  • JavaScript基础:call,apply,bind详解,三者有何区别和联系

    函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比...

    德顺
  • 异步流程控制:7 行代码学会 co 模块

    首先请原谅我的标题党(●—●),tj 大神的 co 模块源码200多行,显然不是我等屌丝能随便几行代码就能重写的。只是当今大家都喜欢《7天学会xx语言》之类的速...

    用户4962466
  • JavaScript进阶之路系列(三):节流防抖

    问题提出: 假如你要提交一个表单,你点击了按钮,出发了提交操作。这时候,你的网络不太好,提交的请求还没得到返回的时候,你又点击了一次按钮,提交了两次,怎么办,...

    刘亦枫

扫码关注云+社区

领取腾讯云代金券