前言
函数式编程与我们以往的编程习惯有许多不同。这篇文章举了一些JavaScript的例子,介绍了函数式编程中重要的概念。附加的文章会让你更深入的了解JavaScript中的函数式编程。
本文源码可以在GitHub上找到,放在jsFunctionalProgramming仓库中。
我要感谢Csaba Hellinger的支持和投入,在他的帮助下我才完成这篇文章。
PART 1
函数式编程由Lambda Calculus演化而来,它是一个抽象数学的函数表述,我们将思考怎么把它运用在现实中。
函数式编程是声明式编程的范式。
为什么要使用函数式编程?
函数式编程有以下具体特性:
避免状态改变(可变的数据) - 函数式编程的特性之一就是:函数在应用中不会改变状态,它们(functions)宁愿从之前的状态之上创建一个新的状态。
函数声明 vs 函数表达式 - 在函数式编程中,我们定义和描述函数就像数学中的一个方法声明。
幂等性 - 这意味着我们用相同的参数调用一个函数(不管任何时刻)它都会返回相同的结果,这也可以避免状态的改变。
这三个特性咋一看似乎并没有什么意义,但如果我们更深入的分析,发现在以下三种情况下使用函数式编程能充分发挥这三个特性:
并行的代码执行 - 因为函数式编程有幂等性和避免状态改变的特性,用函数方法编写代码会让并行更容易,因为不会出现同步问题。
简明、简洁的代码 - 因为函数式编程使用方法声明的方式,代码不会像面向过程编程一样,有额外的算法步骤。
不同的编程思想 - 一旦你真正使用了一门函数式编程语言,你会拥有一种新的编程思想,当你构建应用时也会有新的点子。
f(x) === J(s)
javascript 是一门真正的(纯粹的)函数式编程语言吗?
不!JavaScript并不是一门纯粹的函数式编程语言...
第一型对象 - 函数
它可以很好的运用在函数式编程中,因为函数是第一性对象。如果在一门编程语言中,函数和其他类型一样,那么这门语言中的函数就是第一型对象。举个例子,函数可以作为参数传递给其他函数,也可以赋值给变量。
我们将检查一些函数是否是第一型对象,但是在这之前,我们先构建一个代码块,我们将像真正的函数式语言一样使用JavaScript。
在大部分纯函数式编程语言中(Haskell, Clean, Erlang),它们是没有for
或者while
循环的,所以循环一个列表需要用到递归函数。纯函数式编程语言有语言支持和最好的列表推导式和列表串联。
这里有一个函数实现了for
循环,我们将在接下来的代码中用到它,但是你也将看到它在JS中的局限性,因为尾部调用优化并没有被广泛的支持,但以后会好起来的。
function funcFor(first, last, step, callback) {
//
//递归inner函数
//
function inner(index) {
if((step > 0 && index >= last) || (step < 0 && index < last)) {
return;
}
callback(index);
//
//接下来进行尾部调用
//
inner(index + step);
}
//
//开始递归
//
inner(first);
}
inner
函数包含了对停止递归的管理,它传入参数index
去调用callback
,再递归调用inner(index + step)
确保循环传递到下一步。
递归是函数式编程的一个重要方面。
现在,让我们看看真正的函数式编程:
function applyIfAllNumbers(items, fn) {
if(areNumbers(items)) {
return funcMap(items, fn);
}
return [];
}
applyIfAllNumbers
函数的目的是调用fn
函数,并把items
中的每个数字作为参数传入,但前提是只有在items
数组中都是数字的情况下才去调用。
下面是验证器函数:
function areNumbers(numbers) {
if(numbers.length == 0) {
return true;
}
else {
return isNumber(number[0]) && areNumbers(numbers.slice(1));
}
}
function isNumber(n) {
return isFinite(n) && +n === n;
}
这段代码简单明了,如果参数是一个数字,isNumber
函数返回true
,否则返回false
。areNumbers
函数使用isNumber
函数判断numbers
数组中是否全是数字(再提醒一次,递归常常被用来实现这种逻辑)。
另一个例子是applyForNumbersOnly
:
function applyForNumbersOnly(items, fn) {
let numbers = filter(items, isNumber);
return funcMap(numbers, fn);
}
这样写甚至更简洁:
function applyForNumbersOnly(items, fn) {
return funcMap(filter(items, isNumber), fn);
}
applyForNumbersOnly
调用fn
方法仅仅是为了收集items
中的数字。
funcMap
函数在函数式编程中重现了著名的map
函数,但是这里我借助了funcForEach
函数来创建它:
function funcForEach(items, fn) {
return funcFor(0, items.length, 1, function(idx) {
fn(items[idx]);
});
}
function funcMap(items, fn) {
let result = [];
funcForEach(items, function(item) {
result.push(fn(item));
});
return result;
}
最后还剩filter
函数,我们再一次使用递归来实现过滤的逻辑。
function filter(input, callback) {
function inner(input, callback, index, output) {
if (index === input.length) {
return output;
}
return inner(
input,
callback,
index + 1,
callback(input[index]) ? output.concat(input[index]) : output;
);
}
return inner(input, callback, 0, []);
}
JS中的尾调用优化(TCO)
在EcmaScript 2015 TCO文档中有一些用例的定义,这门语言不久就将支持尾调用优化了。最关键的一点就是在你的代码中使用use strict
模式,否则JS不能支持尾调用优化。
由于没有内置方法来检测浏览器是否支持尾调动优化,以下代码实现了这个功能:
"use static"
function isTCOSupported() {
const outerStackLen = new Error().stack.length;
//inner函数的name长度一定不能超过外部函数
return (function inner() {
const innerStackLen = new Error().stack.length;
return innerStackLen <= outerStackLen;
}());
}
console.log(isTCOSupported() ? "TCO Available" : "TCO N/A");
这里有一个重现Math.pow
函数的例子,它能从EcmaScript 2015的TCO中获益。
这个pow函数的实现使用了ES6默认参数,让它看上去更简洁。
function powES6(base, power, result=base) {
if (power === 0) {
return 1;
}
if(power === 1) {
return result;
}
return powES6(base, power - 1, result * base);
}
首先要提醒以下,powES6
函数有三个参数而不是两个。第三个参数是计算后的值。我们随身携带return
是为了实现让我们的递归调用变成真正的尾调用,让JS可以使用它的尾调用优化技术。
万一我们不能使用ES6的特性,那么我们不推荐使用递归去实现pow
函数,因为这门语言还没有提出有关递归的优化,这样实现起来就很复杂了:
function recursivePow(base, power, result) {
if (power === 0) {
return 1;
}
else if(power === 1) {
return result;
}
return recursivePow(base, power - 1, result * base);
}
function pow(base, power) {
return recursivePow(base, power, base);
}
我们把递归计算放在了另一个recursivePow
函数中,这个函数有三个参数,就像powES6
函数一样。使用一个新函数并把base
作为参数传递给它,以此实现ES6中的默认参数逻辑。
在这个页面你可以查看TCO在不同浏览器和平台的支持情况。
目前只有Safari 10是完全支持TCO的浏览器(在写这篇文章时),我将进行一些对于pow
的测试,来看看它的表现。
测试递归调用
我使用了powES6
和pow
函数来进行测试:
"use strict";
function stressPow(n) {
var result = [];
for (var i=0; i<n; ++i) {
result.push(
pow(2, 0),
pow(2, 1),
pow(2, 2),
pow(2, 3),
pow(2, 4),
pow(2, 5),
pow(2, 10),
pow(2, 20),
pow(2, 30),
pow(1, 10000),
pow(2, 40),
pow(3, 10),
pow(4, 15),
pow(1, 11000),
pow(3.22, 125),
pow(3.1415, 89),
pow(7, 2500),
pow(2, 13000)
);
}
return result;
}
var start = performance.now();
var result_standard = stressPow(2500);
var duration = performance.now() - start;
console.log(result_standard);
console.log(`Duration: ${duration} ms.`);
我在Chrome v55, Firefox v50, Safari v9.2 和 Safari v10上测试了以上代码。
小结
根据上面的数据,我们得出Safari对递归函数的优化效率是最高的。Safari 10对尾调用的支持是最好的,速度比Chrome快了大约2.8倍。Firefox几乎和Safari 9.2 一样棒,这出乎了我的意料。
如果你很喜欢这篇文章,请点个赞哦。(译者注:话说好长啊,好累啊。)
让我们继续函数式!
PART 2 也即将发出,关于高阶函数和例子,讲解如何编写函数式风格的代码。
喜欢本文的朋友可以关注我的微信公众号,不定期推送一些好文。
本文由Rockjins Blog翻译,转载请与译者联系。否则将追究法律责任。