翻译:安歌javascript
校对:咕噜铲屎官前端
译者按:本文采用意译,文章偏理论型,主要从概念上介绍函数式编程,后面计划翻译一篇《JavaScript函数式编程》,从实战中介绍函数式编程,有兴趣能够关注下~java
原文:Master JavaScript Interview: What is Functional Programming? node
正文开始:程序员
“精通JavaScript面试”系列文章是专门为中级晋升高级JavaScript开发的求职者准备的常见考核问题,这些也是我在实际面试中常用的问题。面试
当下,函数式编程已经成为JavaScript界很是热门的一个话题。而在几年前,几乎不多有JavaScript程序员知道什么是函数式编程,可是在我过去三年看到的每个大型应用程序的代码中都大量使用了函数式编程的思想。编程
函数式编程(Functional Programming,一般简称FP)是经过组合纯函数(pure functions)来构建软件的过程,避免共享状态(shared state)、数据可变(mutable date)和反作用(side-effects)。函数式编程是声明式而非命令式的,应用程序的状态经过纯函数流动。与面向对象编程不一样,它的程序状态一般与对象中的方法共享和协做。redux
函数式编程是一种编程范式,这意味着它是基于一些特定的原则(上面列出的)去考虑软件架构的方法论。其余的编程范式还包括面向对象和面向过程编程。数组
相比于命令式或面向对象的代码,函数式代码更加简洁、可预测和易测试,但若是你不熟悉函数式编程以及与之相关的常见模式,那么函数式代码看起来会更加密集,而且与之相关的文章对于新手来讲也很难理解。浏览器
若是你开始在谷歌上搜索函数式编程术语,你很快就会遇到一堵学术术语的砖墙,这对初学者来讲是很是可怕的。但若是你已经用JavaScript编程有一段时间,那么极可能你已经在实际的软件开发中使用了不少函数式编程的思想和实用函数。
不要让全部的生词把你吓跑,这远比它听起来要简单得多。
在开始理解函数式编程的含义以前,你须要理解上面那个看似简单的定义中的几个概念:
换句话说,若是你想知道函数式编程在实践中意味着什么,那你就必须先理解这些核心概念。
纯函数有两个特色:
在函数式编程中纯函数还有不少重要特性,包括引用透明性(指的是函数的运行不依赖于外部变量或状态),阅读“什么是纯函数”了解更多详情。
函数组合是将两个或以上函数组合造成一个新函数的过程。例如,复合函数 f · g
(·
表示一种组合)在JavaScript中等价于f(g(x))
。理解组合函数是使用函数式编程构建软件的重要一步,阅读“什么是组合函数”了解更多详情。
共享状态是指任何变量、对象或内存空间存在与共享做用域下,或者对象的属性在做用域之间传递。共享做用域能够包括全局做用域或闭包做用域,一般,在面向对象编程中,对象经过向其余对象添加属性的方式在做用域之间共享。
好比,计算机游戏可能有一个主游戏对象,其中的角色和道具做为该对象的属性存储。函数式编程避免了共享状态——依赖不可变的数据结构和纯计算从现有数据派生新数据。更多关于函数式的软件如何处理程序状态的信息,查阅“10个更好的Redux架构技巧”。
共享状态的问题在于,为了理解函数的效果,你必须了解函数中使用或影响的每个共享变量的完整历史。
假设你有一个须要保存的user
对象,使用saveUser()
函数向服务器上的一个API发起请求,在这个过程当中,用户使用updateAvatar()
更改头像,并触发了另外一个saveUser()
请求。在保存时,服务器返回一个规范的user
对象,该对象应该替换内存中的任何内容,以便与服务器上发生的更改同步,或者响应其余API的调用。
不幸的是,第二个(saveUser
)响应在第一个(saveUser
)响应以前被接收,因此当第一个(如今已通过时了)响应返回时,新头像将在内存中删除,并被替换为旧的。这是竟态条件中,一个与共享状态相关的很是常见的bug。
与共享状态相关的另外一个常见问题是,更改函数的调用顺序可能会致使级联故障,由于做用于共享状态里面的函数依赖于时序。
// 对于共享状态,函数的调用会影响结果
const x = {
val: 2
};
const x1 = () => x.val += 1;
const x2 = () => x.val *= 2;
x1();
x2();
console.log(x.val); // 6
// 这个例子于上面的相同,除了...
const y = {
val: 2
};
const y1 = () => y.val += 1;
const y2 = () => y.val *= 2;
// 倒置调用顺序
y2();
y1();
console.log(y.val); // 5
复制代码
当避免了共享状态时,函数调用的时序不会改变结果。对于纯函数,给定相同的输入,你老是获得相同的输出,这使得函数调用彻底独立于其余函数的调用,从而在根本上简化修改和重构。
const x = {
val: 2
};
const x1 = x => Object.assign({}, x, { val: x.val + 1});
const x2 = x => Object.assign({}, x, { val: x.val * 2});
console.log(x1(x2(x)).val); // 5
const y = {
val: 2
};
x2(y);
x1(y);
console.log(x1(x2(y)).val); // 5
复制代码
在上面的例子中,咱们使用Object.assign()
而且传递一个空对象做为第一个参数去复制对象x中的属性,在这种状况下,它至关于从零建立一个对象。
若是你仔细看了本例中console.log
语句,你会注意到一些我前面已经提到过的内容:函数组合。前面咱们的函数组合是这样的:f(g(x))
。但在这个例子中,咱们分别用x1()
和x2()
替换了原来的f()
和g(
)以得到了新的组合:x1 · x2
。
固然,若是更改组合函数中的顺序,输出的结果也会相应改变。操做的顺序仍是很重要的。f(g(x))
并不老是等价与g(f(x))
,可是函数外部的变量会发生什么已经变得再也不重要。对于非纯函数,除非知道函数使用或影响的每个变量的完整历史,不然不可能彻底理解这个函数的做用。
消除函数调用的时序依赖性,也就消除了一整类潜在bug。
不可变的对象在其建立后不可再更改,相反地,可变对象是任何建立以后仍然可被修改。 不变性是函数式编程中的核心概念,由于没有它,程序中的数据流就会丢失。状态的历史丢失,奇怪的bug就会蔓延在你的软件中。更多关于不变性的意义,参阅“不变性之道”。(老外也懂道?)
在JavaScript中,不能混淆const和不变性。const建立了一个变量名绑定,该绑定在建立以后不可再赋值。const并无建立一个不可变的对象,咱们仍然能够更改对象的属性,这意味着const建立的绑定对象是可变的,而非不可变。
经过深度冻结对象,可使对象真正不可变。JavaScript中有一个方法能够将对象的第一层进行冻结:
const a = Object.freeze({
foo: 'Hello',
bar: 'world',
baz: '!'
});
a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object
复制代码
译者注:在非
strict
模式下,浏览器或者node环境对a.foo
赋值操做不会引发报错,但也没法修改值。
但冻结的对象只是第一层不可变。例如,下面的对象仍然可变:
const a = Object.freeze({
foo: { greeting: 'Hello' },
bar: 'world',
baz: '!'
});
a.foo.greeting = 'Goodbye';
console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
复制代码
正如你所见,冻结对象中顶层的原始属性不可变,但对于同时也是对象的任何属性(包含Array等)仍然可变——所以即使冻结对象也是可变的,除非遍历整个对象树并冻结每个对象属性。
在许多函数式编程语言中,有一种特殊的不可变数据结构称为tire数据结构(又称字典树,发音tree),它其实是深冻结的——这意味着不论对象的属性层级有多深,属性都不可更改。
Tires(字典树)经过结构共享的方式共享对象中全部节点的内存引用地址,这些内存地址在对象被操做符复制以后保持不变,这将消耗更少的内存,并为某些类型的操做带来显著的性能提高。
例如,你能够在对象树的根位置经过标识进行比较,若是标识相同,则没必要遍历整个树去检查差别。
这里有几个JavaScript库利用了tires数据结构,包括:Immutable.js
和Mori
。 对上面两个库我都进行了实践,在须要大量不可变状态的大型项目中更倾向于使用Immutable.js
。更多关于这方面的信息,参阅“改善Redux体系结构的10个技巧”。
反作用是指在被调用函数以外可被察觉的的除了返回值以外的任何应用程序状态的更改,包括:
函数式编程一般能够避免反作用,这使得程序的效果更易理解和测试。
Haskell和其余函数语言常用Monads(单子)从纯函数中分离和封装反作用。monads的内容足够复杂到能够写一本书,咱们之后再讨论。
译者注:更多关于Monads,参考:前端中的Monad
你如今须要明白的是,反作用的操做须要与软件的其余部分隔离。若是将反作用与程序逻辑部分分开,你的软件将变得更易扩展、重构、调试、测试和维护。
这也是大多数前端框架鼓励用户在独立的、低耦合的模块中管理状态和组件渲染的缘由。
函数式编程倾向于重复利用一组公共函数库来处理数据,面向对象编程则倾向于将对象中的方法和数据放在一块儿,这些方法对操做的数据有类型的限制,而且一般只能对特定类实例中的数据进行操做。
在函数式编程中,全部的数据类型都是对等的。同一个map()
工具函数能够映射对象、字符串、数字或其余数据类型,由于它接收一个函数做为参数,该参数会适当地处理给定的数据类型。
JavaScript中函数是一等公民,容许咱们把函数看成数据看待——把它们赋值给变量、做为参数传入函数或者从函数中返回等等。
高阶函数是以函数为参数、返回函数或者二者兼有的函数。高阶函数一般用于:
函子是可映射的。换句话说,它是一个容器,包含了一个接口,能够将函数应用于其中的值。当你看到“函子”这个词,你就应该想到“可映射的”。
前面咱们了解了同一个map()
能够做用于各类数据类型。它经过函子API提高映射操做来实现这一点。map()
中很重要的流控制操做使用了该接口。对于Array.prototype.map()
,容器是一个数组,固然,其余数据结构也能够是函子——只要它们提供映射API。
让咱们看看Array.prototype.map()
是如何容许你从映射实用程序中抽象数据类型,从而使map()
能够用于任何数据类型的。下面咱们将建立一个简单的double
映射,它会将每个传入的值乘以2:
const double = n => n * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]
复制代码
若是咱们想给下面对象中的点数翻倍呢?咱们所要作的仅是对传递给map()
的double()
函数作一些细微的修改,一切仍然正常:
const double = n => n.points * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([
{ name: 'ball', points: 2 },
{ name: 'coin', points: 3 },
{ name: 'candy', points: 4}
])); // [ 4, 6, 8 ]
复制代码
在函数式编程中,使用诸如函子和高阶函数这样的抽象并利用工具函数操做任意数据类型的思想很是重要。
"流便是随着时间推移而变化的列表"
如今你须要清楚的是,数组和函子并非应用容器和其中的值的概念的惟一方式。例如,数组是一个列表,随着时间推移所表达的列表是一个流——所以你能够应用相同类型的工具函数来处理传入的事件流。当你真正开始使用FP构建软件时,你会常常看到这种状况。
函数式编程是一种声明范式,这意味着程序逻辑的表达不须要显示地描述控制流。
命令式程序花费数行代码去描述用于实现预期结果所需的特定步骤——流程控制:怎么作;
声明式程序抽象了流程控制过程,而且经过数行代码描述数据流:作什么。
例如,这个命令式的映射接受一个数字数组,并返回每一个元素乘以2后的新数组:
const doubleMap = numbers => {
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
return doubled;
};
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
复制代码
这个声明式映射作了一样的事情,可是经过Array.prototype.map()
函数抽象了控制流,使得咱们能够更清晰地表达数据流:
const doubleMap = numbers => numbers.map(n => n * 2);
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
复制代码
命令式代码常用诸如for
、if
、switch
、throw
…这样的语句
而声明式代码更多地依赖于表达式,表达式是一段用于计算某个值的代码,一般是用于产生相应结果值的函数调用、值和运算符的某种组合。
这些都是表达式的例子:
2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
复制代码
一般在代码中,你会看到将表达式赋值给标识符、从函数返回或传递给函数,在赋值、返回或传递以前,首先会对表达式求值,而后使用结果值。
函数式编程支持: