JavaScript 函数式编程

正文

一、什么是函数式编程?

我的理解是函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数可以看成没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的。.函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用。函数式编程我觉得更贴近于数学思想或者说计算。

二、函数式编程的特性

1.函数是"第一等公民"

所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
举例来说,下面代码中的print变量就是一个函数,可以作为另一个函数的参数。

var print = function(i){ console.log(i);};
  [1,2,3].forEach(print);
2.只用”表达式",不用"语句"

"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。
当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

3.没有"副作用"

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

4.不修改状态

在函数式编程中,我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式。这里所说的’变量’是不能被修改的。所有的变量只能被赋一次初值。(下面会详细介绍变量被函数代替)

5.引用透明性

函数程序通常还加强引用透明性,即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。

三、函数式编程常用核心概念和技术

1.纯函数

什么是纯函数呢?

对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态的函数,叫做纯函数。
举个栗子:

var xs = [1,2,3,4,5];// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);// Array.splice会对原array造成影响,所以不纯
xs.splice(0,3);
2.函数柯里化(高级函数)

高阶函数,就是把函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
我的理解:传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

看一个例子:

// 柯里化之前
function add(x, y) {
    return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
    return function (x) {
        return x + y;
    };
}
addX(2)(1) // 3

柯里化可以理解为是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。

3.函数组合

为了解决函数嵌套过深,洋葱代码:h(g(f(x))),我们需要用到“函数组合”,我们可以
来用柯里化来改他,让多个函数像拼积木一样。

函数编程的函数组合:两个纯函数组合之后返回了一个新函数,举个例子:

const compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

const toUpperCase = function(x) {
return x.toUpperCase();
};

const exclaim = function(x) {
return x + "!";
};
 
const shout = compose( exclaim , toUpperCase );
 
console.log(shout("hello world")); //HELLO WORLD!

可以对他简化一下:

const compose=(f,g)=>(x=>f(g(x)))
const toUpperCase=x=>x.toUpperCase()
const exclaim=x=>x+'!'
const shout=compose(exclaim,toUpperCase)
console.log(shout('hello world'));  //HELLO WORLD!

compose中的参数的顺序是随意的,这就类似于乘法中的交换律 xy=yx**,
所以函数式编程贴近于数学计算。

在这里插入图片描述

4.递归与尾递归

指函数内部的最后一个动作是函数调用。 该调用的返回值, 直接返回给函数。 函数调用自身, 称为递归。 如果尾调用自身, 就称为尾递归。 递归需要保存大量的调用记录, 很容易发生栈溢出错误, 如果使用尾递归优化, 将递归变为循环, 那么只需要保存一个调用记录, 这样就不会发生栈溢出错误了。通俗点说,尾递归最后一步需要调用自身,并且之后不能有其他额外操作。
举个例子:我的理解为满足递归条件相当于捕鱼时先撒网,等达到递归边界即捕到鱼时,再返回自身及收网;尾递归就为当捕到鱼时鱼已经到你手里了,具体举个例子:

// 不是尾递归,无法优化
function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}   //这里的递归当到达边界条件时会把值返回给上一个调用它的函数
 
function factorial(n, total) {
    if (n === 1) return total;
    return factorial(n - 1, n * total);
} //ES6强制使用尾递归
// 这里的尾递归就是当到底边界条件时直接将结果 return 出来

我们看一下递归和尾递归执行过程:
递归:

function sum(n) {
    if (n === 1) return 1;
    return n + sum(n - 1);
}
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15      //递归非常消耗内存,因为需要同时保存很多的调用帧,这样,就很容易发生“栈溢出”

尾递归:

function sum(x, total) {
    if (x === 1) {
        return x + total;
    }
    return sum(x - 1, x + total);
}
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15

整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0)。这能有效的防止堆栈溢出。 在ECMAScript 6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。

5.惰性计算(求值)

在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使你可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。简单的来说就是:

按需索取,能不多做事,绝不多做

这里有自己实现的简单的惰性求值
用JavaScript写一个惰性求值的最简实现
惰性求值官方文档
官方文档

四、函数式编程总结

1.函数式编程中的每个符号都是 const 的,于是没有什么函数会有副作用。谁也不能在运行时修改任何东西,也没有函数可以修改在它的作用域之外修改什么值给其他函数继续使用。这意味着决定函数执行结果的唯一因素就是它的返回值,而影响其返回值的唯一因素就是它的参数。
2.函数式编程不需要考虑”死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
3.函数式编程中所有状态就是传给函数的参数,而参数都是储存在栈上的。这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及新的代码获得一个diff,然后用这个diff更新现有的代码,新代码的热部署就完成了。

参考文献

JavaScript函数式编程指南

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352