JavaScript是一门多范式语言,便可使用OOP(面向对象),也可使用FP(函数式),因为笔者最近在学习React相关的技术栈,想进一步深刻了解其思想,因此学习了一些FP相关的知识点,本文纯属我的的读书笔记,若是有错误,望轻喷且提点。javascript
函数式编程(英语:functional programming)
或称函数程序设计、泛函编程,是一种编程范式
,它将计算机运算视为函数运算,而且避免使用程序状态以及易变对象。即对过程进行抽象,将数据以输入输出流的方式封装进过程内部
,从而也下降系统的耦合度。
Js支持FP的一个重要缘由在于,在JS中,函数是一等公民
。即你能够像对其余数据类型同样对其进行操做,把他们存在数组里,看成参数传递,赋值给变量...等等。以下:html
const func = () => {} // 存储 const a = [func] // 参数 返回值 const x = (func) => { ...... ...... return func } x(func)
这个特性在编写语言程序时带来了极大的便利,下面的知识及例子都创建在此基础上。前端
纯函数是这样一种函数,即相同的输入,永远会获得相同的输出,并且没有任何可观察的反作用。
反作用包括但不限于:java
简单一句话, 即只要是与函数外部环境发生交互的都是反作用。 node
像Js中, slice就是纯函数, 而splice则不是ios
var xs = [1,2,3,4,5]; // 纯的 xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] // 不纯的 xs.splice(0,3); //=> [1,2,3] xs.splice(0,3); //=> [4,5] xs.splice(0,3); //=> []
在React生态中,使用纯函数的例子很常见,如React Redner函数,Redux的reducer,Redux-saga的声明式effects等等。 git
React Render
在React中,Render返回了一个JSX表达式,只要输入相同,便可以保证咱们拿到一样的输出
(最终结果渲染到DOM上),而内部的封装细节咱们不须要关心,只要知道它是没有反作用的
,这在咱们开发过程当中带来了极大的便利。当咱们的程序出问题时(渲染出来与预期不符合),咱们只要关心咱们的入参是否有问题便可。编程
class Component extends React.Component { render() { return ( <div /> ) } }
Redux的reducer
Redux的reducer函数要求咱们每一次都要返回一个新的state
, 而且在其中不能有任何反作用,只要传入参数相同,返回计算获得的下一个 state 就必定相同。没有特殊状况、没有反作用,没有 API 请求、没有变量修改,单纯执行计算
。这样作可使得咱们很容易的保存了每一次state改变的状况,对于时间旅行这种需求更是自然的亲近。特别是在调试的过程当中,咱们能够借助插件,任意达到每个state状态
,可以轻松的捕捉到错误是在哪个节点出现。redux
function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) default: return state } }
Redux-sage的声明式effects
许多时候, 咱们会写这样的函数axios
const sendRequest = () => { return axions.post(...) }
这是一个不纯的函数,由于它包含了反作用,发起了http请求,咱们能够这样封装一下:
const sendRequestReducer = () => { return () => { return axios.post(...) } }
ok, 如今是一个纯函数了,正如Redux-saga中的effects同样:
import { call } from 'redux-saga/effects' function* fetchProducts() { const products = yield call(Api.fetch, '/products') // ... }
实际上call不当即执行异步调用,相反,call 建立了一条描述结果的信息
。那么这样作除了增长代码的复杂度,还能够给咱们带来什么?参考saga的官方文档就知道了, 答案是测试:
这些 声明式调用(declarative calls) 的优点是,咱们能够经过简单地遍历 Generator 并在 yield 后的成功的值上面作一个 deepEqual 测试, 就能测试 Saga 中全部的逻辑。这是一个真正的好处,由于复杂的异步操做都再也不是黑盒,你能够详细地测试操做逻辑,无论它有多么复杂。
import { call } from 'redux-saga/effects' import Api from '...' const iterator = fetchProducts() // expects a call instruction assert.deepEqual( iterator.next().value, call(Api.fetch, '/products'), "fetchProducts should yield an Effect call(Api.fetch, './products')" )
纯函数有着如下的优势
可缓存性
首先,纯函数总可以根据输入来作缓存。实现缓存的一种典型方式是 memoize 技术:
var memoize = function(f) { var cache = {}; return function() { var arg_str = JSON.stringify(arguments); cache[arg_str] = cache[arg_str] || f.apply(f, arguments); return cache[arg_str]; }; }; var squareNumber = memoize(function(x){ return x*x; }); squareNumber(4); //=> 16 squareNumber(4); // 从缓存中读取输入值为 4 的结果 //=> 16 squareNumber(5); //=> 25 squareNumber(5); // 从缓存中读取输入值为 5 的结果 //=> 25
可移植性
纯函数由于不依赖外部环境,因此很是便于移植,你能够在任何地方使用它而不须要附带着引入其余不须要的属性。
可测试性
如上面提到的Redux reducer和Redux-saga同样, 它对于测试自然亲近。
并行代码
咱们能够并行运行任意纯函数。由于纯函数根本不须要访问共享的内存,并且根据其定义,纯函数也不会因反作用而进入竞争态(race condition)。
在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把
接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术
。
var add = function(x) { return function(y) { return x + y; }; }; var increment = add(1); var addTen = add(10); increment(2); // 3 addTen(2); // 12
在Lodash类库中,就有这么一个curry函数来帮助咱们处理科里化,关于如何实现一个curry函数,推荐你们参考这篇文章
var abc = function(a, b, c) { return [a, b, c]; }; var curried = _.curry(abc); curried(1)(2)(3); // => [1, 2, 3] curried(1, 2)(3); // => [1, 2, 3] curried(1, 2, 3); // => [1, 2, 3] // Curried with placeholders. curried(1)(_, 3)(2); // => [1, 2, 3]
偏函数自己与科里化并不相关, 但在平常的编写程序中,或许咱们使用更多的是偏函数,因此在这里简单的介绍一下偏函数
偏函数应用是
找一个函数,固定其中的几个参数值,从而获得一个新的函数
。
有时候,咱们会写一个专门发送http请求的函数
const sendRequest = (host, fixPath, path) => { axios.post(`${host}\${fixPath}\{path}`) }
可是大多数时候, host和fixPath是固定的, 咱们不想每次都写一次host和fixPath,但咱们又不能写死,由于咱们须要sendRequest这个函数是能够移植的,不受环境的约束,那么咱们能够这样
const sendRequestPart = (path) => { const host = '...' const fixPath = '...' return sendRequest(host, fixPath, path) }
科里化和偏函数的主要用途是在组合中
,这一小节主要介绍了他们的使用方法和行为。
组合的功能很是强大, 也是函数式编程的一个核心概念, 所谓的对过程进行封装很大程度上就是依赖于组合
。那么什么是组合?
var compose = function(f,g) { return function(x) { return f(g(x)); }; }; var toUpperCase = function(x) { return x.toUpperCase(); }; var exclaim = function(x) { return x + '!'; }; var shout = compose(exclaim, toUpperCase); shout("send in the clowns"); //=> "SEND IN THE CLOWNS!"
上面的compose就是一个最简单的组合函数, 固然组合函数并不限制于传入多少个函数参数,它最后只返回一个函数,我我的更喜欢将它认为像管道同样,将数据通过不一样函数的逐渐加工
,最后获得咱们想要的结果
const testFunc = compose(func1, func2, func3, func4) testFunc(...args)
在js中, 实现compose函数比较容易
const compose = (...fns) => { return (...args) => { let res = args for (let i = fns.length - 1; i > -1; i--) { res = fns[i](res) } return res } }
React官方推崇组合优于继承
这个概念,这里选择两个比较典型的例子来看
React中的高阶组件
在React中,有许多使用高阶组件的地方,如React-router的withRouter函数,React-redux的connect函数返回的函数,
// Navbar 和 Comment都是组件 const NavbarWithRouter = withRouter(Navbar); const ConnectedComment = connect(commentSelector, commentActions)(Comment);
而因为高阶函数的签名是Component => Component
。因此咱们能够很容易的将他们组合到一块儿,这也是官方推荐的作法
// 不要这样作…… const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // ……你可使用一个函数组合工具 // compose(f, g, h) 和 (...args) => f(g(h(...args)))是同样的 const enhance = compose( // 这些都是单独一个参数的高阶组件 withRouter, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)
Redux的compose函数
Redux的compose函数实现要比上面提到的简洁的多
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))) }
这个实现咋看之下有点懵逼, 因此能够拆开来看一下
composeFn = compose(fn1, fn2, fn3, fn4)
那么reduce循环运行时, 第一次a就是fn1, b是fn2, 第二次a是(...args) => fn1(fn2(...args)), b是fn3, 第三次运行的时候则是a是(...args) => fn1(fn2(fn3(...args))), b是fn4, 最后返回了fn1(fn2(fn3(fn4(...args))))
它的意思是说,函数无须说起将要操做的数据是什么样的。
// 非 pointfree,由于提到了数据:word var snakeCase = function (word) { return word.toLowerCase().replace(/\s+/ig, '_'); }; // pointfree var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
pointfree 模式可以帮助咱们减小没必要要的命名,让代码保持简洁和通用。对函数式代码来讲,pointfree 是很是好的石蕊试验,由于它能告诉咱们一个函数是不是接受输入返回输出的小函数。好比,while 循环是不能组合的。不过你也要警戒,pointfree 就像是一把双刃剑,有时候也能混淆视听。并不是全部的函数式代码都是 pointfree 的,不过这不要紧。可使用它的时候就使用,不能使用的时候就用普通函数。
有了组合, 配合上面提到的科里化和偏函数应用, 能够将程序拆成一个个小函数而后组合起来, 优势已经很明显的呈现出来,也很直观的表达出了函数式编程的封装过程的核心概念。
函数式编程创建在范畴学上,不少时候讨论起来不免有点理论化,因此这里简单的介绍一下范畴。
有着如下这些组件(component)的搜集(collection)就构成了一个范畴:
对象的搜集
对象就是数据类型,例如 String、Boolean、Number 和 Object 等等。一般咱们把数据类型视做全部可能的值的一个集合(set)。像 Boolean 就能够看做是 [true, false] 的集合,Number 能够是全部实数的一个集合。把类型看成集合对待是有好处的,由于咱们能够利用集合论(set theory)处理类型。
态射的搜集
态射是标准的、普通的纯函数。
态射的组合
即上面提到的compose
identity 这个独特的态射
让咱们介绍一个名为 id 的实用函数。这个函数接受随便什么输入而后原封不动地返回它:
var id = function(x){ return x; };
在学习函数式编程的时候,第一次看到functor的时候一脸懵逼, 确实不理解这个东西是什么, 能够作什么,加上一堆术语,头都大了。在理解functor以前,先认识一个东西
容器
容器为函数式编程里普通的变量、对象、函数提供了一层极其强大的外衣,赋予了它们一些很惊艳的特性。
var Container = function(x) { this.__value = x; } Container.of = x => new Container(x); //试试看 Container.of(1); //=> Container(1) Container.of('abcd'); //=> Container('abcd')
Container.of 把东西装进容器里以后,因为这一层外壳的阻挡,普通的函数就对他们再也不起做用了
,因此咱们须要加一个接口来让外部的函数也能做用到容器里面的值(像Array也是一个容器):
Container.prototype.fmap = function(f){ return Container.of(f(this.__value)) }
咱们能够这样使用它:
Container.of(3) .fmap(x => x + 1) //=> Container(4) .fmap(x => 'Result is ' + x); //=> Container('Result is 4')
咱们经过简单的代码就实现了一个链式调用,而且这也是一个functor
。
Functor(函子)是实现了 fmap 并遵照一些特定规则的容器类型。
这样子看仍是有点很差理解, 那么参考下面这句话可能会好一点:
a functor is nothing more than a data structure you can map functions over with the purpose of lifting values from a container, modifying them, and then putting them back into a container. 都是些简单的单词,意会比起本人翻译会更容易理解。
加上一张图:
ok, 如今大概知道functor是一个什么样的东西了。
那么functor有什么做用呢?
链式调用
首先它能够链式调用,正如上面提到的同样。
Immutable
能够看到, 咱们每次都是返回了一个新的Container.of, 因此数据是Immutable的, 而Immutable的做用就不在这里赘述了。
将控制权交给Container
将控制权交给Container, 这样他就能够决定什么时候何地怎么去调用
咱们传给fmap的function,这个做用很是强大,能够为咱们作空值判断、异步处理、惰性求值
等一系列麻烦的事。
上面做用的第三点可能直观上有点难以理解, 下面举三个简单的例子
Maybe Container
定义一个Maybe Container来帮咱们处理空值的判断
var Maybe = function(x) { this.__value = x; } Maybe.of = function(x) { return new Maybe(x); } Maybe.prototype.fmap = function(f) { return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value)); } Maybe.prototype.isNothing = function() { return (this.__value === null || this.__value === undefined); } //试试看 import _ from 'lodash'; var add = _.curry(_.add); Maybe.of({name: "Stark"}) .fmap(_.prop("age")) .fmap(add(10)); //=> Maybe(null) Maybe.of({name: "Stark", age: 21}) .fmap(_.prop("age")) .fmap(add(10)); //=> Maybe(31)
固然, 这里能够利用上面提到的科里化函数来简化掉一堆fmap的状况
import _ from 'lodash'; var compose = _.flowRight; var add = _.curry(_.add); // 创造一个柯里化的 map var map = _.curry((f, functor) => functor.fmap(f)); var doEverything = map(compose(add(10), _.property("age"))); var functor = Maybe.of({name: "Stark", age: 21}); doEverything(functor); //=> Maybe(31)
Task Container
咱们能够编写一个Task Container来帮咱们处理异步的状况
var fs = require('fs'); // readFile :: String -> Task(Error, JSON) var readFile = function(filename) { return new Task(function(reject, result) { fs.readFile(filename, 'utf-8', function(err, data) { err ? reject(err) : result(data); }); }); }; readFile("metamorphosis").fmap(split('\n')).fmap(head);
例子中的 reject 和 result 函数分别是失败和成功的回调。正如你看到的,咱们只是简单地调用 Task 的 map 函数,就能操做未来的值,好像这个值就在那儿似的。(这看起来有点像Promise)
Io Container
咱们能够利用Io Container来作惰性求值
import _ from 'lodash'; var compose = _.flowRight; var IO = function(f) { this.__value = f; } IO.of = x => new IO(_ => x); IO.prototype.map = function(f) { return new IO(compose(f, this.__value)) }; var io_document = new IO(_ => window.document); io_document.map(function(doc){ return doc.title }); //=> IO(document.title)
注意咱们这里虽然感受上返回了一个实际的值 IO(document.title),但事实上只是一个对象:{ __value: [Function] },它并无执行,而是简单地把咱们想要的操做存了起来,只有当咱们在真的须要这个值得时候,IO 才会真的开始求值,
functor 的概念来自于范畴学,并知足一些定律。 即functor 接受一个范畴的对象和态射(morphism),而后把它们映射(map)到另外一个范畴里去
。
Js中也有一些实现了functor, 如map、filter
map :: (A -> B) -> Array(A) -> Array(B) filter :: (A -> Boolean) -> Array(A) -> Array(A)
咱们来写一个函数 cat,这个函数的做用和 Linux 命令行下的 cat 同样,读取一个文件,而后打出这个文件的内容
import fs from 'fs'; import _ from 'lodash'; var map = _.curry((f, x) => x.map(f)); var compose = _.flowRight; var readFile = function(filename) { return new IO(_ => fs.readFileSync(filename, 'utf-8')); }; var print = function(x) { return new IO(_ => { console.log(x); return x; }); } var cat = compose(map(print), readFile); cat("file") //=> IO(IO("file的内容"))
ok, 咱们最后获得的是两层嵌套的IO, 要获取其中的值
cat("file").__value().__value()
问题很明显的出来了, 咱们须要连续调用两次_value才能获取, 那么假如咱们嵌套了更多呢, 难道每次都要调用一大堆__value吗, 那固然是不可能的。
咱们可使用一个join函数, 来将Container里面的东西拿出来, 像这样
var join = x => x.join(); IO.prototype.join = function() { return this.__value ? IO.of(null) : this.__value(); } // 试试看 var foo = IO.of(IO.of('123')); foo.join();
彷佛这样也有点麻烦, 每次都要使用一个join来剖析
var doSomething = compose(join, map(f), join, map(g), join, map(h));
咱们可使用一个chain函数, 来帮助咱们作这些事
var chain = _.curry((f, functor) => functor.chain(f)); IO.prototype.chain = function(f) { return this.map(f).join(); } // 如今能够这样调用了 var doSomething = compose(chain(f), chain(g), chain(h)); // 固然,也能够这样 someMonad.chain(f).chain(g).chain(h) // 写成这样是否是很熟悉呢? readFile('file') .chain(x => new IO(_ => { console.log(x); return x; })) .chain(x => new IO(_ => { // 对x作一些事情,而后返回 }))
ok, 事实上这就是一个Monad, 并且你也会很熟悉, 这就像一个Promise的then, 那么什么是Monad呢?
Monad有一个bind方法, 就是上面讲到的chain(同一个东西不一样叫法),
function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> { // ... }
其实,Monad 的做用跟 Functor 相似,也是应用一个函数到一个上下文中的值。不一样之处在于,Functor 应用的是一个接收一个普通值而且返回一个普通值的函数,而 Monad 应用的是一个接收一个普通值可是返回一个在上下文中的值的函数。上下文即一个Container。
须要被认为是Monad须要具有如下三个条件
那么Promise具有了什么条件?
不过Promise比Monad拥有更多的功能。
提到了Functor和Monad而不提Applicative Functor就不完整了。
Applicative Functor就是让不一样 functor 能够相互应用(apply)的能力
。
举一个简单的例子, 假设有两个同类型的 functor,咱们想把这二者做为一个函数的两个参数传递过去来调用这个函数。
// 这样是行不通的,由于 2 和 3 都藏在瓶子里。 add(Container.of(2), Container.of(3)); //NaN // 使用可靠的 map 函数试试 var container_of_add_2 = map(add, Container.of(2)); // Container(add(2))
这时候咱们建立了一个 Container,它内部的值是一个局部调用的(partially applied)的函数。确切点讲就是,咱们想让 Container(add(2)) 中的 add(2) 应用到 Container(3) 中的 3 上来完成调用。也就是说,咱们想把一个 functor 应用到另外一个上。
巧的是,完成这种任务的工具已经存在了,即 chain 函数。咱们能够先 chain 而后再 map 那个局部调用的 add(2),就像这样:
Container.of(2).chain(function(two) { return Container.of(3).map(add(two)); });
然而这样咱们须要延迟Container.of(3)的创建, 这对咱们来讲是很不方便的也是没有必要的, 咱们能够经过创建一个ap函数来达成咱们想要的效果
Container.prototype.ap = function(other_container) { return other_container.map(this.__value); } Container.of(2).map(add).ap(Container.of(3)); // Container(5)
注意上面的add是科里化函数, this.__value是一个纯函数。
因为这种先 map 再 ap 的操做很广泛,咱们能够抽象出一个工具函数 liftA2:
const liftA2 = (f, m1, m2) => m1.map(f).ap(m2) liftA2(add, Container.of(2), Container.of(3))
正如咱们上面所说, 咱们能够独立建立两个Container, 那么在Task中也能够同时发起两个http请求,而没必要等到第一个返回再执行第二个
// Http.get :: String -> Task Error HTML var renderPage = curry(function(destinations, events) { /* render page */ }); Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events')) // Task("<div>some page with dest and events</div>")
Functor
// identity map(id) === id; // composition compose(map(f), map(g)) === map(compose(f, g));
Monad
bind(unit(x), f) ≡ f(x) bind(m, unit) ≡ m bind(bind(m, f), g) ≡ bind(m, x ⇒ bind(f(x), g))
Applicative Functor
Identity: A.of(x => x).ap(v) === v Homomorphism: A.of(f).ap(A.of(x)) === A.of(f(x)) Interchange: u.ap(A.of(y)) === A.of(f => f(y)).ap(u)
如下引用自文章漫谈 JS 函数式编程(一)
面向对象对数据进行抽象,将行为以对象方法的方式封装到数据实体内部,从而下降系统的耦合度。而函数式编程,选择对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也下降系统的耦合度。二者虽是大相径庭,然而在系统设计的目标上能够说是异曲同工的。面向对象思想和函数式编程思想也是不矛盾的,由于一个庞大的系统,可能既要对数据进行抽象,又要对过程进行抽象,或者一个局部适合进行数据抽象,另外一个局部适合进行过程抽象,这都是可能的。数据抽象不必定以对象实体为形式,一样过程抽象也不是说形式上必然是 functional 的,好比流式对象(InputStream、OutputStream)、Express 的 middleware,就带有明显的过程抽象的特征。可是在一般状况下,OOP更适合用来作数据抽象,FP更适合用来作过程抽象。
固然因为Javascript自己是多范式语言, 因此能够在合适的地方使用合适的编程方式。总而言之, 二者互不排斥,是可共存的。
因为函数式编程,若是尾递归不作优化,很容易爆栈, 这个知识点有不少文章提出来了, 这里推荐一篇文章
声明式主要表如今于只关心结果而不关心过程, 这里推荐一篇轻松易懂的文章
或者举个例子:
在JQ时代的时候, 假如咱们须要渲染一个DOM, 并改变其文字颜色, 咱们须要这样的步骤:
而在React中, 咱们能够直接告诉JSX咱们想要DOM的颜色变成红色便可。
const textColor = 'red' const comp = () => { return ( <div style={{ color: textColor }} /> ) }
而关于声明式和函数式, 我我的认为函数式和声明式同样, 也是属于关心结果, 可是函数式最重要的特色是“函数第一位”,即函数能够出如今任何地方。 二者其实不该该作比较。
如下引用自知乎答案
优化绑定
说白了前端和后端不同的关键点是后端HTTP较多,前端渲染多,前端真正的刚需是数据绑定机制。后端一次对话,计算好Response发回就完成任务了,因此后端吃了二十年年MVC老本仍是挺好用的。前端处理的是连续的时间轴,并不是一次对话,像后端那样赋值简单传递就容易断档,致使状态不一致,带来大量额外复杂度和Bug。无论是标准FRP仍是Mobx这种命令式API的TFRP,内部都是基于函数式设计的。函数式从新发明的Return和分号是要比裸命令式好得多的(前端状态能够同步,后端线程安全等等,想怎么封装就怎么封装)。
封装做用
接上条,大幅简化异步,IO,渲染等做用/反作用相关代码。和不少人想象的不同,函数式很擅长处理做用,只是多一层抽象,若是应用稍微复杂一点,这点成本很快就能找回来(Redux Saga是个例子,特别是你写测试的状况下)。渲染如今你们均可以理解幂等渲染地好处了,其实函数式编程各类做用和状态也是幂等的,对于复杂应用很是有帮助。
复用
引用透明,无反作用,代数设计让函数式代码能够正确优雅地复用。前端不像后端业务固定,作好业务分析和DDD就能够搭个静态结构,高枕无忧了。前端的好代码必定是活的,每处均可能乱改。可组合性其实很重要。经过高阶函数来组合效果和效率都要高于继承,试着多用ramda,你就能够发现绝大部分东西都能一行写完,最后给个实参就变成一个UI,来需求改两笔就变成另一个。
函数式编程在JS的将来是大放异彩仍是泯然众人,都不影响咱们学习它的思想。本文里面有许多引用没有特别指出,但都会在底部放上连接(如介意请留言), 望见谅。
声明式编程和命令式编程有什么区别?
用 JS 代码完整解释 Monad
怎么理解“声明式渲染”?
JavaScript函数式编程(二)
JavaScript Functors Explained
前端开发js函数式编程真实用途体如今哪里?
js 是更倾向于函数式编程了仍是更倾向于面向对象?或者没有倾向?只是简单的提供了更多的语法糖?
漫谈 JS 函数式编程(一)
有哪些函数式编程在前端的实践经验?
前端使用面向对象式编程 仍是 函数式编程 针对什么问题用什么方式 分别有什么具体案例?
什么是 Monad (Functional Programming)?
Monads In Javascript
Functor、Applicative 和 Monad
JavaScript 让 Monad 更简单
函数式编程