深入探讨javascript函数式编程

有时,优雅的实现是一个函数。不是方法。不是类。不是框架。只是函数。
                      - John Carmack,游戏《毁灭战士》首席程序员

函数式编程全都是关于如何把一个问题分解为一系列函数的。通常,函数会链在一起,互相嵌套, 来回传递,被视作头等公民。如果你使用过诸如jQuery或Node.js这样的框架,你应该用过一些这样的技术, 只不过你没有意识到。

我们从Javascript的一个小尴尬开始。

假设我们需要一个值的列表,这些值会赋值给普通的对象。这些对象可能包含任何东西:数据、HTML对象等等。

var
  obj1 = {value: 1},
  obj2 = {value: 2},
  obj3 = {value: 3};
var values = [];
function accumulate(obj) {
 values.push(obj.value);
}
accumulate(obj1);
accumulate(obj2);
console.log(values); // Output: [obj1.value, obj2.value]

这个代码能用但是不稳定。任何代码都可以不通过accumulate()函数改变values对象。 而且如果我们忘记了给values赋上空数组[],这个代码压根儿就不会工作。

但是如果变量声明在函数内部,他就不会被任何捣蛋的代码给更改。

function accumulate2(obj) {
 var values = [];
 values.push(obj.value);
 return values;
}
console.log(accumulate2(obj1)); // Returns: [obj1.value]
console.log(accumulate2(obj2)); // Returns: [obj2.value]
console.log(accumulate2(obj3)); // Returns: [obj3.value]

不行呀!只有最后传入的那个对象的值才被返回。

我们也许可以通过在第一个函数内部嵌套一个函数来解决这个问题。

var ValueAccumulator = function(obj) {
 var values = []
 var accumulate = function() {
  values.push(obj.value);
 };
 accumulate();
 return values;
};

可是问题依然存在,而且我们现在无法访问accumulate函数和values变量了。

我们需要的是一个自调用函数

自调用函数和闭包

如果我们能够返回一个可以依次返回values数组的函数表达式怎么样?在函数内声明的变量可以被函数内的所有代码访问到, 包括自调用函数。

通过使用自调用函数,前面的尴尬消失了。

var ValueAccumulator = function() {
 var values = [];
 var accumulate = function(obj) {
  if (obj) {
   values.push(obj.value);
   return values;
  } else {
   return values;
  }
 };
 return accumulate;
};
//This allows us to do this:
var accumulator = ValueAccumulator();
accumulator(obj1);
accumulator(obj2);
console.log(accumulator());
// Output: [obj1.value, obj2.value]
ValueAccumulator = ->
 values = []
 (obj) ->
  values.push obj.value if obj
  values

这些都是关于作用域的。变量values在内部函数accumulate()中可见,即便是在外部的代码在调用这个函数时。 这叫做闭包。

Javascript中的闭包就是函数可以访问父作用域,哪怕父函数已经执行完毕。

闭包是所有函数式语言都具有的特征。传统的命令式语言没有闭包。

高阶函数

自调用函数实际上是高阶函数的一种形式。高阶函数就是以其它函数为输入,或者返回一个函数为输出的函数。

高阶函数在传统的编程中并不常见。当命令式程序员使用循环来迭代数组的时候,函数式程序员会采用完全不同的一种实现方式。 通过高阶函数,数组中的每一个元素可以被应用到一个函数上,并返回新的数组。

这是函数式编程的中心思想。高阶函数具有把逻辑像对象一样传递给函数的能力。

在Javascript中,函数被作为头等公民对待,这和Scheme、Haskell等经典函数是语言一样的。 这话听起来可能有点古怪,其实实际意思就是函数被当做基本类型,就像数字和对象一样。 如果数字和对象可以被来回传递,那么函数也可以。

来实际看看。现在把上一节的ValueAccumulator()函数配合高阶函数使用:
// 使用forEach()来遍历一个数组,并对其每个元素调用回调函数accumulator2
var accumulator2 = ValueAccumulator();
var objects = [obj1, obj2, obj3]; // 这个数组可以很大
objects.forEach(accumulator2);
console.log(accumulator2());

纯函数

纯函数返回的计算结果仅与传入的参数相关。这里不会使用外部的变量和全局状态,并且没有副作用。 换句话说就是不能改变作为输入传入的变量。所以,程序里只能使用纯函数返回的值。

用数学函数来举一个简单的例子。Math.sqrt(4)将总是返回2,不使用任何隐藏的信息,如设置或状态, 而且不会带来任何副作用。

纯函数是对数学上的“函数”的真实演绎,就是输入和输出的关系。它们思路简单也便于重用。 由于纯函数是完全独立的,它们更适合被一次又一次地使用。

举例说明来对比一下非纯函数和纯函数。

// 把信息打印到屏幕中央的函数
var printCenter = function(str) {
 var elem = document.createElement("div");
 elem.textContent = str;
 elem.style.position = 'absolute';
 elem.style.top = window.innerHeight / 2 + "px";
 elem.style.left = window.innerWidth / 2 + "px";
 document.body.appendChild(elem);
};
printCenter('hello world');
// 纯函数完成相同的事情
var printSomewhere = function(str, height, width) {
 var elem = document.createElement("div");
 elem.textContent = str;
 elem.style.position = 'absolute';
 elem.style.top = height;
 elem.style.left = width;
 return elem;
};
document.body.appendChild(
printSomewhere('hello world',
window.innerHeight / 2) + 10 + "px",
window.innerWidth / 2) + 10 + "px"));

非纯函数依赖window对象的状态来计算宽度和高度,自给自足的纯函数则要求这些值作为参数传入。 实际上它就允许了信息打印到任何地方,这也让这个函数有了更多用途。

非纯函数看起来是一个更容易的选择,因为它在自己内部实现了追加元素,而不是返回元素。 返回了值的纯函数printSomewhere()则会在跟其他函数式编程技术的配合下有更好的表现。

var messages = ['Hi', 'Hello', 'Sup', 'Hey', 'Hola'];
messages.map(function(s, i) {
 return printSomewhere(s, 100 * i * 10, 100 * i * 10);
}).forEach(function(element) {
 document.body.appendChild(element);
});

当一个函数是纯的,也就是不依赖于状态和环境,我们就不用管它实际是什么时候被计算出来。 后面的惰性求值将讲到这个。

匿名函数

把函数作为头等对象的另一个好处是匿名函数。

就像名字暗示的那样,匿名函数就是没有名字的函数。实际不止这些。它允许了在现场定义临时逻辑的能力。 通常这带来的好处就是方便:如果一个函数只用一次,没有必要给它浪费一个变量名。

下面是一些匿名函数的例子:

// 写匿名函数的标准方式
function() {
 return "hello world"
};

// 匿名函数可以赋值给变量
var anon = function(x, y) {
 return x + y
};

// 匿名函数用于代替具名回调函数,这是匿名函数的一个更常见的用处
setInterval(function() {
 console.log(new Date().getTime())
}, 1000);
// Output: 1413249010672, 1413249010673, 1413249010674, ...

// 如果没有把它包含在一个匿名函数中,他将立刻被执行,
// 并且返回一个undefined作为回调函数:
setInterval(console.log(new Date().getTime()), 1000)
// Output: 1413249010671

下面是匿名函数和高阶函数配合使用的例子

function powersOf(x) {
 return function(y) {
  // this is an anonymous function!
  return Math.pow(x, y);
 };
}
powerOfTwo = powersOf(2);
console.log(powerOfTwo(1)); // 2
console.log(powerOfTwo(2)); // 4
console.log(powerOfTwo(3)); // 8
powerOfThree = powersOf(3);
console.log(powerOfThree(3)); // 9
console.log(powerOfThree(10)); // 59049

这里返回的那个函数不需要命名,它可以在powersOf()函数外的任何地方使用,这就是匿名函数。

还记得累加器的那个函数吗?它可以用匿名函数重写

var
 obj1 = { value: 1 },
 obj2 = { value: 2 },
 obj3 = { value: 3 };

var values = (function() {
 // 匿名函数
 var values = [];
 return function(obj) {
  // 有一个匿名函数!
  if (obj) {
   values.push(obj.value);
   return values;
  } else {
   return values;
  }
 }
})(); // 让它自执行
console.log(values(obj1)); // Returns: [obj.value]
console.log(values(obj2)); // Returns: [obj.value, obj2.value]
obj1 = { value: 1 }
obj2 = { value: 2 }
obj3 = { value: 3 }

values = do ->
 valueList = []
 (obj) ->
  valueList.push obj.value if obj
  valueList
console.log(values(obj1)); # Returns: [obj.value]
console.log(values(obj2)); # Returns: [obj.value, obj2.value]

真棒!一个高阶匿名纯函数。我们怎么这么幸运?实际上还不止这些,这里面还有个自执行的结构, (function(){...})();。函数后面跟的那个括号可以让函数立即执行。在上面的例子里, 给外面values赋的值是函数执行的结果。

匿名函数不仅仅是语法糖,他们是lambda演算的化身。请听我说下去…… lambda演算早在计算机和计算机语言被发明的很久以前就出现了。它只是个研究函数的数学概念。 非同寻常的是,尽管它只定义了三种表达式:变量引用,函数调用和匿名函数,但它被发现是图灵完整的。 如今,lambda演算处于所有函数式语言的核心,包括javascript。
 由于这个原因,匿名函数往往被称作lambda表达式。

匿名函数也有一个缺点,那就是他们在调用栈中难以被识别,这会对调试造成一些困难。要小心使用匿名函数。

方法链

在Javascript中,把方法链在一起很常见。如果你使用过jQuery,你应该用过这种技巧。它有时也被叫做“建造者模式”。

这种技术用于简化多个函数依次应用于一个对象的代码。

// 每个函数占用一行来调用,不如……
arr = [1, 2, 3, 4];
arr1 = arr.reverse();
arr2 = arr1.concat([5, 6]);
arr3 = arr2.map(Math.sqrt);

// ……把它们串到一起放在一行里面
console.log([1, 2, 3, 4].reverse().concat([5, 6]).map(Math.sqrt));
// 括号也许可以说明是怎么回事
console.log(((([1, 2, 3, 4]).reverse()).concat([5, 6])).map(Math.sqrt));

这只有在函数是目标对象所拥有的方法时才有效。如果你要创建自己的函数,比如要把两个数组zip到一起, 你必须把它声明为Array.prototype对象的成员.看一下下面的代码片段:
Array.prototype.zip = function(arr2) {
  // ...
}

这样我们就可以写成下面的样子
arr.zip([11,12,13,14).map(function(n){return n*2});
// Output: 2, 22, 4, 24, 6, 26, 8, 28

递归

递归应该是最著名的函数式编程技术。就是一个函数调用它自己。

当函数调用自己,有时奇怪的事情就发生了。它的表现即是一个循环,多次执行同样的代码,也是一个函数栈。

使用递归函数时必须十分小心地避免无限循环(这里应该说是无限递归)。就像循环一样,必须有个停止条件。 这叫做基准情形(base case)。

下面有个例子

var foo = function(n) {
 if (n < 0) {
  // 基准情形
  return 'hello';
 } else {
  // 递归情形
  return foo(n - 1);
 }
}
console.log(foo(5));

译注:原文中的代码有误,递归情形的函数调用缺少return,导致函数执行得最后没有结果。这里已经纠正。

递归和循环可以相互转换。但是递归算法往往更合适,甚至是必要的,因为有些情形用循环很费劲。

一个明显的例子就是遍历树。

var getLeafs = function(node) {
 if (node.childNodes.length == 0) {
  // base case
  return node.innerText;
 } else {
  // recursive case:
  return node.childNodes.map(getLeafs);
 }
}

分而治之

递归不只是代替for和while循环的有趣的方式。有个叫分而治之的算法,它递归地把问题拆分成更小的情形, 直到小到可以解决。

历史上有个欧几里得算法用于找出两个数的最大公分母

function gcd(a, b) {
 if (b == 0) {
  // 基准情形 (治)
  return a;
 } else {
  // 递归情形 (分)
  return gcd(b, a % b);
 }
}

console.log(gcd(12,8));
console.log(gcd(100,20));

gcb = (a, b) -> if b is 0 then a else gcb(b, a % b)

理论上来说,分而治之很牛逼,但是现实中有用吗?当然!用Javascript的函数对数组排序不是很好, 它不但替换了原数组,也就是说数据不是不变的,并且它还不够可靠、灵活。通过分而治之,我们可以做得更好。

全部的实现代码大概要40行,这里只展示伪代码:

var mergeSort = function(arr) {
 if (arr.length < 2) {
  // 基准情形: 只有0或1个元素的数组是不用排序的
  return items;
 } else {
  // 递归情形: 把数组拆分、排序、合并
  var middle = Math.floor(arr.length / 2);
  // 分
  var left = mergeSort(arr.slice(0, middle));
  var right = mergeSort(arr.slice(middle));
  // 治
  // merge是一个辅助函数,返回一个新数组,它将两个数组合并到一起
  return merge(left, right);
 }
}

译注:关于用分而治之的思路进行排序的一个更好的例子是快排,使用Javascript也只有13行代码。 具体请参考我以前的博文 《优雅的函数式编程语言》

惰性求值

惰性求值,也叫做非严格求值,它会按需调用并推迟执行,它是一种直到需要时才计算函数结果的求值策略, 这对函数式编程特别有用。比如有行代码是 x = func(),调用这个func()函数得到的返回值会赋值给x。 但是x等于什么一开始并不重要,直到需要用到x的时候。等到需要用x的时候才调用func()就是惰性求值。

这一策略可以让性能明显增强,特别是当使用方法链和数组这些函数式程序员最喜爱的程序流技术的时候。 惰性求值让人兴奋的一个优点是让无限序列成为可能。因为在它实在无法继续延迟之前,什么都不需要被真正计算出来。 它可以是这个样子:

// 理想化的JavaScript伪代码:
var infinateNums = range(1 to infinity);
var tenPrimes = infinateNums.getPrimeNumbers().first(10);

这为很多可能性敞开了大门,比如异步执行、并行计算、组合,这只列举了一点。

然而,还有个问题,Javascript本身并不支持惰性求值,也就是说存在让Javascript模拟惰性求值的函数库, 这是第三章的主题。

(0)

相关推荐

  • Javascript函数式编程语言

    函数式编程语言 函数式编程语言是那些方便于使用函数式编程范式的语言.简单来说,如果具备函数式编程所需的特征, 它就可以被称为函数式语言.在多数情况下,编程的风格实际上决定了一个程序是否是函数式的. 是什么让一个语言具有函数式特征? 函数式编程无法用C语言来实现.函数式编程也无法用Java来实现(不包括那些通过大量变通手段实现的近似函数式编程). 这些语言不包含支持函数式编程的结构.他们是纯面向对象的.严格非函数式的语言. 同时,纯函数语言也无法使用面向对象编程,比如Scheme.Haskell以

  • 《JavaScript函数式编程》读后感

    本文章记录本人在学习 函数式 中理解到的一些东西,加深记忆和并且整理记录下来,方便之后的复习. 在近期看到了<JavaScript函数式编程>这本书预售的时候就定了下来.主要目的是个人目前还是不理解什么是函数式编程.在自己学习的过程中一直听到身边的人说面向过程编程和面向对象编程,而函数式就非常少.为了自己不要落后于其他同学的脚步,故想以写笔记的方式去分享和记录自己阅读中所汲取的知识. js 和函数式编程 书中用了一句简单的话来回答了什么是函数式编程: 函数式编程通过使用函数来将值转换为抽象单元

  • JavaScript的函数式编程基础指南

    引言 JavaScript是一种强大的,却被误解的编程语言.一些人喜欢说它是一个面向对象的编程语言,或者它是一个函数式编程语言.另外一些人喜欢说,它不是一个面向对象的编程语言,或者它不是一个函数式编程语言.还有人认为它兼具面向对象语言和函数式语言的特点,或者,认为它既不是面向对象的也不是函数式的,好吧,让我们先搁置那些争论. 让我们假设我们共有这样的一个使命:在JavaScript语言所允许的范围内,尽可能多的使用函数式编程的原则来编写程序. 首先,我们需要清理下脑子里那些关于函数式编程的错误观

  • javascript函数式编程实例分析

    本文实例讲述了javascript函数式编程.分享给大家供大家参考.具体分析如下: js像其他动态语言一样是可以写高阶函数的,所谓高阶函数是可以操作函数的函数.因为在js中函数是一个彻彻底底的对象,属于第一类公民,这提供了函数式编程的先决条件. 下面给出一个例子代码,出自一本js教程,功能是计算数组元素的平均值和标准差,先列出非函数式编程的一种写法: var data = [1,1,3,5,5]; var total = 0; for(var i = 0;i < data.length;i++)

  • Javascript函数式编程简单介绍

    几十年来,函数式编程一直是计算机科学狂热者的至爱,由于数学的纯洁性和谜一般的本质, 它被埋藏在计算机实验室,只有数据学家和有希望获得博士学位的人士使用.但是现在,它正经历一场复兴, 这要感谢一些现代语言比如Python,Julia,Ruby,Clojure以及--但不是最后一个--Javascript. 你是说Javascript?这个WEB脚本语言?没错! Javascript已经被证明是一项长期以来都没有消失的重要的技术.这主要是由于它扩展的一些框架和库而使其具有重生的能力, 比如backb

  • JavaScript 函数式编程的原理

    1,JavaScript中函数.方法的调用 在JavaScript中,有两种调用函数的方式.一般的方式是把参数放在括号中,另一种方式是同时把函数和参数都放在括号中.如: 复制代码 代码如下: function test(x) { alert(x); } test("hello"); (test)("hello"); //等同于下面的代码 (function test(x) { alert(x); })("hello"); //也等同于下面的代码

  • javascript函数式编程程序员的工具集

    如果你仔细看了到目前为止出现过的示例代码,你会发现这里面的一些方法不太熟悉. 它们是map().filter()和reduce()函数,它们对任何语言的函数式编程都至关重要. 它们可以让你不必使用循环和语句,写出更简洁的代码. map().filter()和reduce()函数组成了函数式程序员工具集的核心部分,这个工具集包括一系列纯的. 高阶的函数,它们是函数式方法的主力.实际上,它们是纯函数和高阶函数的典型,它们以一个函数为输入, 返回一个输出结果,并且不产生副作用. 然而它们是浏览器中EC

  • 浅谈javascript函数式编程

    函数式编程,属于编程范式的一种 1 函数是第一公民,可以返回值,也可以作为其他函数的参数 //console是一个函数 function con(v){ console.log(v) } // execute 也是一个函数 function execute(fn){ fn(1) } //将con函数作为参数传进execute函数 execute(con) // 1 2 接近自然语言的写法 晓池吃完饭然后就去洗澡 可以表现为eat().bathe() // 吃饭函数 function eat(ea

  • 探究JavaScript函数式编程的乐趣

    编程范式 编程范式是一个由思考问题以及实现问题愿景的工具组成的框架.很多现代语言都是聚范式(或者说多重范式): 他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等. 函数式编程范式 函数式编程就像一辆氢燃料驱动的汽车--先进的未来派,但是还没有被广泛推广.与命令式编程相反,他由一系列语句组成,这些语句用于更新执行时的全局状态.函数式编程将计算转化作表达式求值.这些表达式全由纯数学函数组成,这些数学函数都是一流的(可以被当做一般值来运用和处理),并且没有副作用. 函数式编程

  • 理解javascript函数式编程中的闭包(closure)

    闭包(closure)是函数式编程中的概念,出现于 20 世纪 60 年代,最早实现闭包的语言是 Scheme,它是 LISP 的一种方言.之后闭包特性被其他语言广泛吸纳. 闭包的严格定义是"由函数(环境)及其封闭的自由变量组成的集合体."这个定义对于大家来说有些晦涩难懂,所以让我们先通过例子和不那么严格的解释来说明什么是闭包,然后再举例说明一些闭包的经典用途. 什么是闭包 通俗地讲, JavaScript 中每个的函数都是一个闭包,但通常意义上嵌套的函数更能够体 现出闭包的特性,请看

随机推荐