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

文章目录

  • 3.1.2 箭头函数——更流行的方式 Arrow functions - the modern way
    • 1. 返回值 Returning values
    • 2. this 值的处理 Handling the this value
    • 3. arguments 的处理 Working with arguments
    • 4. 单参数还是多参数? One argument or many?

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

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),才得到最终结果。如前所述,该函数执行的运算与之前看到的版本是一样的,但实现方式上却有着天壤之别。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/888811.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【rCore OS 开源操作系统】Rust 字符串(可变字符串String与字符串切片str)

【rCore OS 开源操作系统】Rust 语法详解: Strings 前言 这次涉及到的题目相对来说比较有深度&#xff0c;涉及到 Rust 新手们容易困惑的点。 这一次在直接开始做题之前&#xff0c;先来学习下字符串相关的知识。 Rust 的字符串 Rust中“字符串”这个概念涉及多种类型&…

k8s 中的金丝雀发布(灰度发布)

目录 1 什么是金丝雀发布 2 Canary 发布方式 3 Canary 两种发布方式实操 3.1 准备工作 3.1.1 将 nginx 命名两个版本 v1 与 v2 3.1.2 暴露端口并指定微服务类型 3.1.3 进入 pod 修改默认发布文件 3.1.4 测试 service 是否正常 3.2 基于权重的灰度发布 3.2.1 创建 Igress 资源类…

分享我“Excel 表格”关键字的博客笔记(python脚本全程自动)

Python脚本全程自动&#xff0c;全部Python内建工具脚本纯净。 (笔记模板由python脚本于2024年10月05日 19:51:06创建&#xff0c;本篇笔记适合喜欢Excel和Python的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大…

ubuntu双系统分区划分

EFI系统分区&#xff08;Windows&#xff09;&#xff1a;自Windows 8起&#xff0c;UEFI模式下的BIOS使用该分区。简单来说&#xff0c;它用于存储已安装系统的EFI引导程序。此分区在资源管理器中无法查看&#xff0c;因为它没有驱动器号&#xff0c;但它必须存在&#xff0c;…

前端登录页面验证码

首先&#xff0c;在el-form-item里有两个div&#xff0c;各占一半&#xff0c;左边填验证码&#xff0c;右边生成验证码 <el-form-item prop"code"><div style"display: flex " prop"code"><el-input placeholder"请输入验证…

[Offsec Lab] ICMP Monitorr-RCE+hping3权限提升

信息收集 IP AddressOpening Ports192.168.52.218TCP:22,80 $ nmap -p- 192.168.52.218 --min-rate 1000 -sC -sV -Pn PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.9p1 Debian 10deb10u2 (protocol 2.0) | ssh-hostkey: | 2048 de:b5:23:89:bb:9f:d4:1…

数据校验的总结

业务层进行复杂检查 简单校验交给Controller校验&#xff0c;能流到业务的层的数据就是基本合法 引入依赖&#xff1a;spring-boot-starter-validation 能标注的所有注解在这两个地方看 jakarta.validation.constraints、 org.hibernate.validator.constraints 使用步骤…

Web APIs——Dom获取属性操作

1.变量声明 1.1以后声明变量我们优先使用哪个&#xff1f; const 有了变量先给const&#xff0c;如果发现它后面是要被修改的&#xff0c;再改为let 1.2 为什么const声明的对象可以修改里面的属性&#xff1f; 因为对象是引用类型&#xff0c;里面存储的是地址&#x…

2024 ciscn WP

一、MISC 1.火锅链观光打卡 打开后连接自己的钱包&#xff0c;然后点击开始游戏&#xff0c;答题八次后点击获取NFT&#xff0c;得到有flag的图片 没什么多说的&#xff0c;知识问答题 兑换 NFT Flag{y0u_ar3_hotpot_K1ng} 2.Power Trajectory Diagram 方法1&#xff1a; 使用p…

《Programming from the Ground Up》阅读笔记:p147-p180

《Programming from the Ground Up》学习第9天&#xff0c;p147-p180总结&#xff0c;总计34页。 一、技术总结 1.Physical memeory p152, Physical memory refers to the actual RAM chips inside your computer and what they contain. 物理地址指的RAM&#xff0c;即我们…

库函数相关(上一篇补充)

一、创建自己的头文件 在当前目录下创建一个my_head.h将这个文件移动到/usr/include目录 #ifndef __MY_HEAD_H__ #define __MY_HEAD_H__#include <stdio.h> #include <errno.h> #include <string.h>#define PRINT_ERR(s) do{\printf("%s %s %d\n&quo…

minio简单使用

文章目录 简介官方地址Linux下载安装安装服务启动关闭帮助命令 java开发minio依赖包新建项目pom配置文件配置类Service测试类运行测试 Api使用前言针对桶的操作查看某个桶是否存在创建一个桶返回桶列表删除一个桶 针对文件的操作上传文件到桶中(本地文件上传)上传文件到桶中(基…

猴子吃桃-C语言

1.问题&#xff1a; 猴子第一天摘下若干个桃子&#xff0c;当即吃了一半&#xff0c;还不过瘾&#xff0c;又多吃了一个。 第二天早上又将剩下的桃子吃掉一半&#xff0c;又多吃一个。以后每天早上都吃了前一天剩下的一半零一个。 到第N天早上想再吃时&#xff0c;见只剩下一个…

【学习笔记】一种使用多项式快速计算 sin 和 cos 近似值的方法

一种使用多项式快速计算 sin 和 cos 近似值的方法 在嵌入式开发、游戏开发或其他需要快速数学计算的领域&#xff0c;sin 和 cos 函数的计算时间可能会影响程序的整体性能。特别是在对时间敏感、精度要求不高的场景中&#xff0c;传统的 sin 和 cos 函数由于依赖复杂的数值方法…

【UE】简单介绍“Extra Win Function”插件的功能

“Extra Win Function”插件包含32个C类封住成的蓝图节点供用户使用&#xff0c;下面简单介绍19个可能常用的节点的功能。 1. “Is Internet Available” 检查是否可接入互联网 2. “Get Device Platform” 获取设备平台名称 3. “Get Android Device RAMSize” 获取RAM 大小 …

【Java SE基础回顾】看这篇就够了!

JavaSE复习 参考视频&#xff1a;【狂神说Java】JavaSE阶段回顾总结 简单的就不讲了&#xff0c;比如什么break和continue区别&#xff0c;甚至一些什么什么继承封装多态的概念等等。 主要写一些Java特有的&#xff0c;重点的基础知识。主要还是例子和代码更多&#xff0c;更有…

Android Preference的使用以及解析

简单使用 values.arrays.xml <?xml version"1.0" encoding"utf-8"?> <resources><string-array name"list_entries"><item>Option 1</item><item>Option 2</item><item>Option 3</item&…

衡石分析平台系统管理手册-智能运维之系统设置

系统设置​ HENGSHI 系统设置中展示了系统运行时的一些参数&#xff0c;包括主程序相关信息&#xff0c;Base URL、HTTP 代理、图表数据缓存周期、数据集缓存大小、租户引擎等相关信息。 主程序​ 系统设置中展示了主程序相关信息&#xff0c;这些信息是系统自动生成的&#…

Linux 之 Linux应用编程概念、文件IO、标准IO

Linux应用编程概念、文件IO、标准IO 学习任务&#xff1a; 1、 学习Linux 应用开发概念&#xff0c;什么是系统调用&#xff0c;什么是库函数 2、 学习文件IO&#xff1a;包括 read、write、open、close、lseek 3、 深入文件IO&#xff1a;错误处理、exit 等 4、 学习标准IO&a…

TCP四次挥手过程详解

TCP四次挥手全过程 有几点需要澄清&#xff1a; 1.首先&#xff0c;tcp四次挥手只有主动和被动方之分&#xff0c;没有客户端和服务端的概念 2.其次&#xff0c;发送报文段是tcp协议栈的行为&#xff0c;用户态调用close会陷入到内核态 3.再者&#xff0c;图中的情况前提是双…