Javascript 中的函数式编程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Javascript 中的函数式编程相关的知识,希望对你有一定的参考价值。

本文和大家分享的主要是javascript中函数式编程相关内容,一起来看看吧,希望对大家学习javascript有所帮助。
函数式编程(functional programming)或称函数程序设计,又称泛函编程,是一种编程范型,比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
函数式编程,近年来一直被炒得火热,国内外的开发者好像都在议论和提倡这种编程范式。在众多的函数式语言中,Javascript 无疑是最亮眼的一个,越来越多的人开始学习和拥抱它,并使用它运用函数式编程来开发实际的大型应用,开源社区也源源不断的诞生函数式风格的框架和类库(Angular / React / Redux)。
作为 web 平台唯一的标准通用语言,Javascript 在软件历史上掀起了最大的语言热潮,拥有当下最大的开源包管理工具(npm)的Javascript 也从 Lisp 手中接过了维持数十年的 最流行的函数式编程语言” 的名号。在Javascript 的世界中是天然支持函数式编程的,函数式编程的基本特征有:
· 一等函数
· 闭包
· 高阶函数
· 纯度
本文会以 Javascript 为例子,和大家一起来了解和学习函数式编程。
一等函数(First Class Functions)
一等函数这个术语最早在20世纪60年代,由英国计算机科学家 Christopher Strachey  functions as first-class citizens 一文中提出的。意思是指,函数和其他一等公民(Number / String...)一样,拥有和它们一样的能力和作用:
·函数储存为变量
const foo = () => {...}
·函数可以储存为数据的一个元素
const arr = [1, 2, () => {...}]
·函数可以作为对象的属性值
const obj = {name: ’xx’, say: () => {}}
·函数可以在使用时直接创建出来
1 + (() => { return 2; })()
·函数可以作为变量传递给另一个函数
bar (name, fun) { fun(name) }
bar(’xx’, (name) => { console.log(name) })
·函数可以被另一个函数返回
foo() {
return () => {...}
}
在函数式编程中,函数是作为基本单元,并且在函数之上建立代码和数据的封装,以提高应用的重用和灵活性。支持一等函数的作用是显而易见的,我们可以使用函数去完成大部分的功能。
闭包(Closure)
历经了 30年,闭包终于成为了编程语言的主要特点。但是根据一项调查显示,有关 Javascript 闭包的问题占了 23% 左右,对于相当数量的开发者来说闭包仍然模糊而又神秘。对于闭包解释我还是更倾向于 Kyle Simpson的系列书 You Don’t Know JavaScript 中的解释:
函数在被定义时是可以访问当前的词法作用域,当函数离开作用域之外被执行时,就形成了闭包。
简而言之,闭包就是一个函数,捕获了作用域内的外部绑定。来看个例子:
function student (people) {
return (name) => { return people[name] }
}var someone = student({xx: {age: 20}, jackson: {age: 21}})
someone(’xx’) // {age: 20}
在执行完 student 函数后,里面的匿名函数形成了一个闭包,闭包是可以访问到 people 对象。闭包为Javascript 提供了私有访问,这让给开发者建立数据抽象提供了极大地便利,也可以更好地书写函数式代码,建立更加强大的代码。
来思考一个场景,手头上拥有一个书本的数组,数组里面包含了书本的信息,现在需要做的是找出把书名填充到一个数组中并且返回,我们一般都会这样写:
const books = [{title: ’人类简史’, author: ’zz’}, {title: ’禅与摩托车维修艺术
books.map((item) => { return item.title })
我们使用了 Array.prototype.map 方法,传入了一个匿名函数,函数中 return 了书名 title。假如需要利用闭包来进一步抽象的话,要怎么写呢?
function plucker (key) {
return  (obj) => {
return (obj && obj[key])
}
}
books.map(plucker(’title’))
我们定义了一个 plucker 函数,它接收一个 key 参数并返回一个匿名函数,匿名函数就是一个闭包并补捕获了 key 参数。在利用了闭包的情况下,我们可以传入任意想要的书本信息(比如:plucker(’author’)),这样就提高了代码的重用性和灵活性。当我们对于闭包认识足够充分时并合理运用到实际开发中去,将会切身体会到闭包的威力和它给我们带来的便利。
高阶函数(Higher Order Functions)
在数学和计算机科学中,高阶函数式至少满足下列一个条件的函数:
· 接受一个或多个函数作为输入
· 输出一个函数
在上述的 plucker 函数就是一个例子,还有我们熟知的 Array.prototype 相关的方法,比如 .map.sort 等等都是高阶函数,因为它们满足接受一个函数作为参数的条件。
那么先来看一个一阶函数的例子,定义一个函数,它会将数组中4个字母的单词给过滤掉:
const words = [’foo’, ’bar’, ’test’, ’some’]; const filter = words => {
let arr = [];
for(let i = 0, { length } = words; i < length; i++) {
const word = word;
if(word.length !== 4) {
arr.push(word);
}
}
return arr;
}
filter(words); // [’foo’, ’bar’]
假如现在又需要过滤数组中,以 ‘b’ 字母开头的单词?那么再定义一个函数:
const startWith = words => {
let arr = [];
for(let i = 0, { length } = arr; i < length; i++) {
const word = word;
if(word.indexOf(’b’) !== 0) {
arr.push(word);
}
}
return arr;
}
filter(words); // [’foo’, ’test’, ’some’]
根据上面两个函数的对比来看,其实主要代码的逻辑都是相似的,先遍历数组再进行条件判断,最后 push到数组中。其实,遍历和过滤都可以抽象出来,可以方便其他的类似函数去调用,毕竟在数组中根据条件过滤是很常见的需求。
const reduce = (reducer, init, arr) => {
let acc = init;
for(let i = 0,{ length } = arr; i < length; i++) {
acc = reducer(acc, arr);
}
return acc;
}
reduce((acc, curr) => acc + curr, 0, [1, 2, 3]);    // 6
如果使用过 Underscore 库的话,就会发现 reduce 和 Underscore.reduce 作用是一样的,实现的是累计的功能。reduce 接受了 3个参数:ruducer 函数、累计的初始值和一个数组,遍历时将每个数组元素作为 reducer 的参数传入,返回值又赋值给累计变量 init,遍历完成时也就完成了累计的功能。
现在如果将 rudece 应用到第一个需求上(过滤四个字母的单词):
const func = (fn ,arr) => {
return reduce((acc, curr) => fn(curr) ? acc.concat([curr]) : acc, [], arr)
}console.log(func(word => word.length !== 4, words)); // ["foo", "bar"]
可以发现,将公共代码抽象出来之后,filter 的函数实现非常简洁,只需传入不同的条件函数,就能为我们去处理符合各种条件的数据。高阶函数可以用来实现函数的多态性,并且相对于一阶函数,高阶函数的复用性和灵活性更好。
纯度(Purity)
函数式编程不仅仅只关心函数,也是思考如何尽量地降低软件复杂性的一种方式。在一些函数式编程语言中,纯度是被强制执行的,不允许使用有副作用的表达式。但是在 Javascript 中,纯度必须通过管理区实现,并且非常容易在偶然间创建和使用非纯函数。
一个纯函数需要满足以下三个条件:
· 函数结果只能通过参数来计算得出
· 不能依赖于能被外部操作改变的数据
· 不能改变外部状态
根据这上述条件来看,在 Javascript 的世界中去维持绝对纯净是不可能的,因为缺少了大多数函数式语言中使用的高效、不变的数据结构。我们知道在 Javascript 拥有能力去 freeze() 对象,但是只能对接对象的顶级属性,这就意味着一个嵌套对象下的属性是仍然能够被更改的。
var obj = Object.freeze({
foo: ’hello’,
bar: {
text: ’world’
}
})
obj.foo = ’goodbye’;console.log(obj.foo); // hello
obj.bar.text = ’goobye’;console.log(obj.bar.text); // goodbye
 ES6 中新增的 const 关键字,使用 const 可以定义一个不能够被重新赋值为不同的值,但是一个 const 对象的属性还是可变的。
const obj = ’hello’;
obj = ’goodbye’;    // Uncaught TypeError: Assignment to constant variable.
const obj = {
foo: ’hello’,
bar: ’world’
}
obj.foo = ’goodbye’;
console.log(obj);     // {foo: ’goodbye’, bar: ’world’}
 Javascrpt 中实现综合不变性还有很长的路要走。换句话来说,虽然不能够保证绝对的纯净,但是我们可以将纯净的部分抽离出来,将变化的影响降到最低,使得代码变得更加通用和容易测试。
总结:
· 函数式编程是支持一等函数的,函数具有其他数据类型相同的功能
· 函数式编程中使用闭包来进行数据的封装
· 使用高阶函数来建立代码的抽象,使代码更加灵活通用
· 尽量抽离纯函数来保持代码的可测性和通用性
来源:稀土掘金

以上是关于Javascript 中的函数式编程的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript函数式编程(纯函数柯里化以及组合函数)

javascript函数式编程简单介绍

翻译JavaScript 中的函数式编程:函数组合与柯里化

我眼中的 JavaScript 函数式编程

函数式编程

JavaScript函数式编程:基本思想