【玩转 JS 函数式编程_008】3.1.2 JavaScript 函数式编程筑基之:箭头函数——一种更流行的写法

写在前面
故天将降大任于是人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能。
——《孟子·告子章句下·第十五节》

3.1.2 箭头函数——更流行的方式 Arrow functions - the modern way

尽管箭头函数工作原理与其他函数几乎殊无二致,但与普通函数相比还是存在一些关键差异(详见 箭头函数 MDN 文档)。箭头函数可以不带 return 语句隐式地返回某个值、同时也没有绑定 this(即函数的上下文)的操作、且不存在 arguments 对象;它们不能被用作构造函数(constructors),也没有 prototype 属性(property),并且由于不允许使用 yield 关键字,也无法用作生成器函数(generators)。

本节我们将讨论以下几个与 JavaScript 函数相关的话题:

  1. 如何返回不同的函数值;
  2. 如何处理 this 值带来的问题;
  3. 参数个数不固定时的处理;
  4. 一个重要概念:科里化currying)(后续章节将多次用到)。

1. 返回值 Returning values

根据 Lambda 演算的编码风格,函数仅由一个结果构成。简化起见,新增的箭头函数也提供了相应的语法支持。当写作 (x, y, z) => 并后跟一个表达式时,就隐式包含了一个 return 语句。例如下面的两个函数就与前面演示的 sum3() 函数功能相同:

const f1 = (x: number, y: number, z: number): number =>
  x + y + z;
const f2 = (x: number, y: number, z: number): number => {
  return x + y + z;
};

如若返回的是一个对象,则必须添加小括号,否则 JavaScript 会误以为后面跟的是代码。为了避免您认为这是个小概率事件,请参阅本章最后 思考题 中的 问题3.1。这是一个非常常见的情况!

关于代码风格的说明

在定义一个单参数函数时,参数两边的小括号可以忽略。为保持风格一致,笔者更倾向于始终保留小括号。然而,本书使用的格式化工具 Prettier(第一章《入门函数式编程的若干问题》提到过)最初倾向于默认不保留;但在 2.0 版本中,配置项 arrow-parens 的默认值已由先前的 avoid(尽量不使用小括号)改为了 always(始终保留小括号)。

2. this 值的处理 Handling the this value

JavaScript 的一个经典问题是 this 值的处理,该取值往往并不按您的“套路”出牌。最终 ES2015 通过箭头函数成功解决了 this 的指向问题。来看下面这个例子:当超时函数被调用时,this 会指向全局变量(window)而非新的对象,因此控制台输出的是 undefined

function ShowItself1(identity: string) {
  this.identity = identity;
  setTimeout(function () {
    console.log(this.identity);
  }, 1000);
}
var x = new ShowItself1("Functional");
// 一秒后显示 undefined,而非 Functional

解决这个问题,传统 JavaScript 有两个经典方案,此外还有一个新增的箭头函数的方案:

  • 传统方案一:利用闭包的特性,定义一个局部变量(通常命名为 thatself),这样该变量就能获取到 this 的原始值,而非 undefined
  • 传统方案二:使用 bind() 函数,将超时函数的 this 绑定到正确的值上(上一节介绍《λ表达式与函数》时也有类似应用);
  • 箭头函数版:这是更新潮的写法,无需其他改动就能获取到正确的 this 值(直接指向对象)。

三种方案的代码实现如下:第一个 timeout 函数使用了闭包,第二个用到了函数绑定,第三个则用到了箭头函数:

// 接上段代码...

function ShowItself2(identity: string) {
  this.identity = identity;
  const that = this;
  setTimeout(function () {
    console.log(that.identity);
  }, 1000);

  setTimeout(
    function () {
      console.log(this.identity);
    }.bind(this),
    2000
  );

  setTimeout(() => {
    console.log(this.identity);
  }, 3000);
}

const x2 = new ShowItself2("JavaScript");
// 一秒后显示 "JavaScript"
// 再过一秒同样显示 "JavaScript"
// 又过一秒还是显示 "JavaScript"

运行上述代码,控制台将在一秒后出现 JavaScript;然后又过了一秒,再次看到 JavaScript;再过 1 秒,控制台会第三次看到 JavaScript

图 1 三种解决方案在控制台中的实际执行情况

【图 1 三种解决方案在控制台中的实际执行情况】

三种方法都能正确运行,具体选哪一个视个人喜好决定。

3. arguments 的处理 Working with arguments

前两章介绍过展开运算符(...)的一些用法。然而,它最常见的使用场景却是与 arguments 对象的处理密切相关(后续第六章会详述)。先来重温上一章的 once() 函数:

// once.ts
const once = <FNType extends (...args: any[]) => any>(
  fn: FNType
) => {
  let done = false;
  return ((...args: Parameters<FNType>) => {
    if (!done) {
      done = true;
      return fn(...args);
    }
  }) as FNType;
};

为什么在写了 return (...args) => 这句后,第 9 行又来一个 func(...args) 呢?这与当下主流观点在处理 函数参数个数不固定 时的具体方式有关,包括没有参数的情况在内。那么,老版本的 JavaScript 是怎么处理这个问题的呢?答案是 arguments 对象(注意它 不是 数组,详见 MDN 官方文档),通过它来访问到实际传入的参数。

arguments 恰巧是一个 类数组对象(array-like object),并不是真正的数组——它唯一拥有的数组属性,便是 length;除此之外,arguments 无法调用 map()forEach() 等任何数组方法。要将 arguments 转换为真正的数组,则必须使用 slice() 方法,并通过 apply() 方法来调用另一个函数,如下所示:

function somethingElse() {
  // get arguments and do something
}

function useArguments() {
  ...
  var myArray = Array.prototype.slice.call(arguments);
  somethingElse.apply(null, myArray);
  ...
}

而使用新版 JavaScript 语法,则无需考虑 argumentsslice 以及 apply

function useArguments2(...args) {
  ...
  somethingElse(...args);
  ...
}

查看上述代码您需要牢记以下三点:

  • 写下 listArguments2(...args) 表明新函数将接收若干个参数(也可能不带参数);
  • 无需任何手动处理就能获得一个参数数组;args 是一个真正的数组;
  • 写成 somethingElse(...args) 比写成 apply() 更加清晰明了。

顺便提一下,当前版本的 JavaScript 依旧支持 arguments 的使用,若要用它来创建数组,有两种替代方案可以实现,不必使用 Array.prototype.slice.call

  • 使用 from() 方法,具体写作:myArray = Array.from(arguments)
  • 直接写作:myArray = [... arguments],这也是扩展运算符的另一种用法。

在后续介绍高阶函数、需要用函数来处理其它函数时,如果遇到参数数量不固定的情况,上述写法会变得非常普遍。

JavaScript 为这类问题提供了简洁高效的写法,因此必须尽快熟悉。这笔投资相当划算!

4. 单参数还是多参数? One argument or many?

编写一个返回值为函数的函数也是可行的,后续第六章还会见到更多这样的情况。例如,按照 λ 算子的演算要求,当中用到的函数没有参数为多个的情况,只接受一个参数;这时就可以通过一项称为 柯里化(currying) 的处理技术来解决这个问题(这么做是何用意?这里先卖个关子,暂且按下不表)。

拓展:双重嘉奖

科里化(Currying)得名于这一概念的提出者 Haskell Curry。值得一提的是,另一门函数式编程语言也被冠名为 Haskell —— 这也算是对其杰出贡献的双重认可(double recognition),可谓梅开二度!

举个例子,之前演示过的三数求和的函数就可以写成下列形式:

// sum3.ts
const altSum3 = (x: number) => (y: number) => (z: number)
  =>
    x + y + z;

这里的函数为什么重命名了呢?简单来说,它已经与之前定义的函数 sum3() 不一样了:sum3() 的类型为 (x: number, y: number, z: number) => number;而 altSum3() 的类型则是 (x: number) => (y: number) => (z: number) => number,二者截然不同(了解更多信息,可参阅本章最后的思考题 3.3)。尽管如此,后者也能得到与之前完全相同的结果。来看看它的具体用法。例如,要对数字 1、2、3 求和:

altSum3(1)(2)(3); // 6

思考

继续往下读之前,不妨思考一下:如果执行的是 altSum3(1, 2, 3),会得到什么样的结果?提示:结果并非是数字!完整答案参见下文。

该函数是怎么运行的呢?不妨将其拆分为多次调用,这也是上面那句表达式在 JavaScript 解释器上的实际计算方式:

const fn1 = altSum3(1);
const fn2 = fn1(2);
const fn3 = fn2(3);

开动您的函数式思维大脑!根据定义,调用 altSum3(1) 的结果,应该是一个 函数。该函数利用了闭包,可以等效解析为如下形式:

const fn1 = y => z => 1 + y + z;

此时的 altSum3() 函数只单独接受一个参数,而非三个参数;其运行结果,fn1,也是一个只接受单个参数的新函数。再运行 fn1(2) ,结果同样是一个函数,同样也只接受一个参数,它等效于:

const fn2 = z => 1 + 2 + z;

再运行 fn2(3),才得到最终结果。如前所述,该函数执行的运算与之前看到的版本是一样的,但实现方式上却有着天壤之别。

您可能会觉得柯里化只是一种取巧的操作罢了:谁会只调用单参数的函数呢?在本书后续第八章《函数的连接》和第十二章《构建更好的容器》讲到如何将函数连接在一起时,您就能明白这么做的根本原因了,届时将多个参数从上一步传递到下一步的操作是无效的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安冬的码畜日常

您的鼓励是我持续优质内容的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值