前言
JavaScript 是一门多范式语言,即可使用 OOP(面向对象),也可以使用 FP(函数式),由于笔者最近在学习 React 相关的技术栈,想进一步深入了解其思想,所以学习了一些 FP 相关的知识点,本文纯属个人的读书笔记,如果有错误,望轻喷且提点。
什么是函数式编程
函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。即对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。
为什么 Js 支持 FP
Js 支持 FP 的一个重要原因在于,在 JS 中,函数是一等公民。即你可以像对其他数据类型一样对其进行操作,把他们存在数组里,当作参数传递,赋值给变量 … 等等。如下:
const func = () => {}
// 存储
const a = [func]
// 参数 返回值
const x = (func) => {
……
……
return func
}
x(func)
这个特性在编写语言程序时带来了极大的便利,下面的知识及例子都建立在此基础上。
纯函数
概念
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。副作用包括但不限于:
打印 /log
发送一个 http 请求
可变数据
DOM 查询
简单一句话,即只要是与函数外部环境发生交互的都是副作用。
像 Js 中,slice 就是纯函数,而 splice 则不是
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 等等。
React Render 在 React 中,Render 返回了一个 JSX 表达式,只要输入相同,即可以保证我们拿到同样的输出(最终结果渲染到 DOM 上),而内部的封装细节我们不需要关心,只要知道它是没有副作用的,这在我们开发过程中带来了极大的便利。当我们的程序出问题时(渲染出来与预期不符合),我们只要关心我们的入参是否有问题即可。
class Component extends React.Component {
render() {
return (
<div />
)
}
}
Redux 的 reducer Redux 的 reducer 函数要求我们每一次都要返回一个新的 state, 并且在其中不能有任何副作用,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。这样做可以使得我们很容易的保存了每一次 state 改变的情况,对于时间旅行这种需求更是天然的亲近。特别是在调试的过程中,我们可以借助插件,任意达到每一个 state 状态,能够轻松的捕捉到错误是在哪一个节点出现。
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 许多时候,我们会写这样的函数
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)
}
总结
科里化和偏函数的主要用途是在组合中,这一小节主要介绍了他们的使用方法和行为。
组合 compose
组合的功能非常强大,也是函数式编程的一个核心概念,所谓的对过程进行封装很大程度上就是依赖于组合。那么什么是组合?
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
它的意思是说,函数无须提及将要操作的数据是什么样的。
// 非 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)就构成了一个范畴:
对象的搜集
态射的搜集
态射的组合
identity 这个独特的态射
对象的搜集 对象就是数据类型,例如 String、Boolean、Number 和 Object 等等。通常我们把数据类型视作所有可能的值的一个集合(set)。像 Boolean 就可以看作是 [true, false] 的集合,Number 可以是所有实数的一个集合。把类型当作集合对待是有好处的,因为我们可以利用集合论(set theory)处理类型。
态射的搜集 态射是标准的、普通的纯函数。
态射的组合 即上面提到的 compose
identity 这个独特的态射 让我们介绍一个名为 id 的实用函数。这个函数接受随便什么输入然后原封不动地返回它:
var id = function(x){return x;};
functor
在学习函数式编程的时候,第一次看到 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 的概念来自于范畴学,并满足一些定律。即 functor 接受一个范畴的对象和态射(morphism),然后把它们映射(map)到另一个范畴里去。
Js 中的 functor
Js 中也有一些实现了 functor, 如 map、filter
map :: (A -> B) -> Array(A) -> Array(B)
filter :: (A -> Boolean) -> Array(A) -> Array(A)
Monad
普通 functor 的问题
我们来写一个函数 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。
Promise 是 Monad
需要被认为是 Monad 需要具备以下三个条件
拥有容器,即 Maybe、IO 之类。
一个可以将普通类型转换为具有上下文的值的函数,即 Contanier.of
拥有 bind 函数(即上面提到的 bind,而不是 ES5 的 bind)
那么 Promise 具备了什么条件?
拥有容器 Promise, 即上面第一点
Promise.resolve(value) 将值转换为一个具有上下文的值,即上面第二点。
Promise.prototype.then(onFullfill: value => Promise) 拥有一个 bind(then)函数,接受一个函数作为参数,该函数接受一个普通值并返回一个含有上下文的值。即上面第三点
不过 Promise 比 Monad 拥有更多的功能。
如果 then 返回了一个正常的 value,Promise 会调用 Promise.resolve 将其转换为 Promise
普通的 Monad 只能提供在计算的时候传递一个值,而 Promise 有两个不同的值 – 一个用于成功值,一个用于错误(类似于 Either monad)。可以使用 then 方法的第二个回调或使用特殊的.catch 方法捕获错误
Applicative Functor
提到了 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>”)
FunctorMonadApplicative Functor 的数学规律
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 与 函数式和面向对象
以下引用自文章漫谈 JS 函数式编程(一)
面向对象对数据进行抽象,将行为以对象方法的方式封装到数据实体内部,从而降低系统的耦合度。而函数式编程,选择对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。两者虽是截然不同,然而在系统设计的目标上可以说是殊途同归的。面向对象思想和函数式编程思想也是不矛盾的,因为一个庞大的系统,可能既要对数据进行抽象,又要对过程进行抽象,或者一个局部适合进行数据抽象,另一个局部适合进行过程抽象,这都是可能的。数据抽象不一定以对象实体为形式,同样过程抽象也不是说形式上必然是 functional 的,比如流式对象(InputStream、OutputStream)、Express 的 middleware,就带有明显的过程抽象的特征。但是在通常情况下,OOP 更适合用来做数据抽象,FP 更适合用来做过程抽象。
当然由于 Javascript 本身是多范式语言,所以可以在合适的地方使用合适的编程方式。总而言之,两者互不排斥,是可共存的。
尾递归优化
由于函数式编程,如果尾递归不做优化,很容易爆栈,这个知识点有很多文章提出来了,这里推荐一篇文章
声明式编程
声明式主要表现在于只关心结果而不关心过程,这里推荐一篇轻松易懂的文章 或者举个例子: 在 JQ 时代的时候,假如我们需要渲染一个 DOM,并改变其文字颜色, 我们需要这样的步骤:
找到 DOM 的 class 或者 id
根据 class 或者 id 找到 DOM
重新赋值 DOM 的 style 属性的 color 属性
而在 React 中,我们可以直接告诉 JSX 我们想要 DOM 的颜色变成红色即可。
const textColor = ‘red’
const comp = () => {
return (
<div style={{
color: textColor
}} />
)
}
而关于声明式和函数式,我个人认为函数式和声明式一样,也是属于关心结果,但是函数式最重要的特点是“函数第一位”,即函数可以出现在任何地方。两者其实不应该做比较。
函数式编程在 JS 中的实践
Undescore/Lodash/Ramda 库 特别是 Lodash,打开 node_modules 基本都能看到
Immutable-js 数据不可变
React
Redux
ES6 尾递归优化
函数式编程在前端开发中的优势
以下引用自知乎答案
优化绑定
说白了前端和后端不一样的关键点是后端 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 更简单 函数式编程