写在前面
上一章,我们通过一个实际问题,相当于让 JS 函数式编程来了一段“才艺展示”。要想让它真正成为瑞士军刀级的好工具,还得扎扎实实锤炼 FP 基本功——这就是第三章的主要任务——带您重新认识 JS 中的函数相关概念,为后续高级话题的引入做好铺垫。
第三章 从函数开始——核心概念剖析
上一章我们用了一个案例来介绍函数式编程思想,本章来看看函数式编程的基础知识,并回顾一下函数的相关知识点。第一章提过,JavaScript
的两个重要特性:
- 函数作一等对象;
- 函数作闭包。
本章涉及要点:
JavaScript
中的函数,包括如何定义(尤其是箭头函数);- 函数科里化,以及作一等对象的函数;
- 以函数式编程的方式使用函数的几种方法。
学完这些内容后,您将了解与函数相关的通用及特定概念。它们是函数式编程的核心。
3.1. 函数面面观 All about functions
首先简要回顾一下 JavaScript
中函数的相关知识,厘清函数与函数式编程之间的联系。在第一章的重点论述、以及第二章的多处介绍中,我们曾提到函数可以作为一等对象,进一步考察了它们在实际编码中的使用。本节重点关注以下三个方面:
- 关于 λ 算子的一些重要的基础概念——它们是函数式编程的理论基础;
- 箭头函数——是
Lambda
算子在JavaScript
语言的最直接的诠释; - 视函数为一等对象——函数式编程中的一个关键概念。
3.1.1. Lambda
表达式与函数 Of lambdas and functions
一个函数按 Lambda 演算的术语要求,可以表示为:λx.2 * x
其中字母 λ 后的变量相当于函数的参数,句点后的表达式,是可以替换作为参数传递的任意值的地方。稍后您将看到本例按 JavaScript
箭头函数的语法,可以写作:x => 2 * x
,形式上十分类似。
提示:arguments 与 parameter
分清参数
arguments
与参数parameter
之间的区别,可以借助一些押韵的顺口溜来强化记忆:Parameters are Potential, Arguments are Actual(参数是潜在的,参数是实际的)。parameter
是要传递的潜在值的占位符,而arguments
是实际传给函数的值。换句话说,定义函数时,您列出的参数是parameter
;而当调用它时,需要提供arguments
。
应用一个函数,是指如同平时写代码那样,使用括号向其提供实际的参数(arguments
)。例如,(λx.2 * x ) (3) 的值为 6。这些 Lambda
函数在 JavaScript
下的等效形式是什么样的呢?这是个值得探讨的问题,因为定义函数的方法有好几种,并且它们在含义上并不完全相同。
拓展阅读
有篇不错的文章介绍了函数及方法定义的多种不同方式,不妨了解一下,具体详见 Leo Balter 及 Rick Waldron 的文章:The Many Faces of Functions in JavaScript(
JavaScript
中函数的多种表现形式)。
您能用多少种方式定义一个 JavaScript
函数呢?答案可能比您原以为的要多。至少可以写出以下几种:
- 具名函数声明:
function first(...) {...};
- 匿名函数表达式:
var second = function(...) {...};
- 具名函数表达式:
var third = function someName(...) {...};
IIFE
立即引用表达式:var fourth = (function() { ...; return function(...) {...}; })();
- 构造函数:
var fifth = new Function(...);
- 箭头函数:
var sixth = (...) => {...};
此外,你还也可以添加对象方法声明,因为它们也隐含了函数的成分。不过上述清单已经够用了。
那么,这些函数定义方法的区别在哪儿?为什么值得关注呢?让我们一个一个来考察:
提示
JavaScript
还可以定义生成器函数(generator function
),形如function*(...) {...}
,返回一个生成器对象Generator
;以及定义异步函数(async function
),其实质为generator
与promise
的结合。本节不使用这三种函数,更多详情参考MDN
这两篇文档:
-
第一种定义,以关键字
function
开头的独立声明形式,应该是JavaScript
中最常见的定义方式。这里定义了一个名为first
的函数(即first.name=="first"
)由于变量提升效应,该函数将在其定义的作用域内可随处任意访问。关于变量提升效应,详见 MDN 文档,只需记住一点:该效应只作用于变量声明,而非变量初始化。 -
第二种定义,将函数赋给一个变量,也能得到一个函数,只不过是个匿名函数(即没有命名)。然而不少
JavaScript
引擎是可以推断出函数名称的,例如设定本例中的second.name === "second"
。(观察下面的示例代码,函数名称没有赋给匿名函数)鉴于变量提升效应不提升变量赋值,该函数只能在赋值之后的代码位置方可访问;再者,您可能更倾向于使用const
而非var
来定义该变量,因为不太会(也不应该)改变该函数:var second = function() {}; console.log(second.name); // "second" var myArray = new Array(3); myArray[1] = function() {}; console.log(myArray[1].name); // ""
-
第三种定义,与第二种相同,只是此时的函数拥有其自己的名称:
third.name === "someName"
。
提示
函数的名称与您调用的时机有关,在执行递归调用时更是如此。第九章《函数设计——递归》中还会详细论述这一点。如果只想要一个回调函数,您可以使用一个没有名称的函数;但要注意,在错误回溯中,当您试图了解代码报错时使用的列表类型,以及哪个函数调用了什么的时候,命名函数更容易被识别。
-
第四种定义的
IIFE
立即引用表达式使用了闭包的知识。内部函数可以以完全私有、被封装的方式,访问在其外部函数中定义的变量或其他函数。回顾第一章闭包一节定义的计次函数,可以编写如下代码:var myCounter = (function(initialValue = 0) { let count = initialValue; return function() { count++; return count; }; })(77); myCounter(); // 78 myCounter(); // 79 myCounter(); // 80
仔细钻研这段代码,外部函数接收一个参数 77 作为
count
的初值(默认为 0)。由于闭包的缘故,内部函数可以访问到count
。从各方面看,返回的函数都是一个通用函数——唯一的区别在于它访问了私有元素。 这也是模块型设计模式(module pattern
)的基础。 -
第五类定义由于不安全,还是不用的好。这种方式首先传入参数列表,然后是一个字符串形式的函数体,用到了与调用
eval()
相同的方式创建函数。这可能招致很多危险的黑客攻击,所以不要用这类定义。为满足读者的好奇心,这里给出一个根据第一章展开运算符小节重写后的sum3
函数示例:var sum3 = new Function("x", "y", "z", "var t = x + y + z; return t;"); sum3(4, 6, 7); // 17
拓展
这类定义不仅不安全,还有其他坑——不会在其创建函数的上下文中创建闭包,因此这些私有变量都是全局的。详见 MDN 文档。切记,用这种方式创建函数是下下策。
续:MDN 原文为:使用
Function
构造函数创建的函数不会为其创建上下文(creation context
)创建闭包; 它们总是在全局范围内创建。 运行它们时,它们将只能访问自己的局部变量和全局变量,而不能访问创建Function
构造函数的作用域内的变量。 这不同于将eval()
与函数表达式的代码一起使用。以下是文档提供的示例代码:
var x = 10; function createFunction1() { var x = 20; return new Function('return x;'); // this |x| refers global |x| } function createFunction2() { var x = 20; function f() { return x; // this |x| refers local |x| above } return f; } var f1 = createFunction1(); console.log(f1()); // 10 var f2 = createFunction2(); console.log(f2()); // 20
-
最后一种定义,使用带
=>
符号的箭头函数定义,是代码最为紧凑的一种定义方式,也是我们将尽可能尝试使用的方式。
至此,我们已经考察了定义函数的若干种方式,接下来重点关注箭头函数,这也是全书力荐的一种代码风格。