浅析函数式编程与前端

简介: 浅析函数式编程与前端

什么是函数式编程


我们常见的编程范式有两种:命令式和声明式,比如我们熟悉的面向对象思想就属于命令式,而函数式编程属于声明式。而且顺带说一句,函数式编程里面提到的“函数”不是我们理解的编程中的“function”概念,而是数学中的函数,即变量之间的映射。

那么,函数式编程和我们熟知的声明式编程区别是什么?引用知乎用户的一句话来做总结:函数式编程关心数据的映射,命令式编程关心解决问题的步骤

举个例子,如果编写一个函数来实现把数组的每个数字都变成它本身的2倍,命令式编程的思路应该是:遍历一次数组,并且把每个数字乘以2,代码如下:

const solution = (arr) => {
  const newArr = [];
  for(let i = 0;i < arr.length;i++) {
    newArr.push(arr[i]*2);
  }
  return newArr;
}
复制代码

但是如果从函数式编程的思维去思考,无非就是数组A的每个元素是数组B每个元素的两倍,存在一个映射:[a, b, c, d, ...] => [2a, 2b, 2c, 2d, ...],代码如下:

const solution = (arr) => {
  return arr.map(item => {
    return item*2;
  })
}
复制代码

从上面这两个简单的例子可以看出来,函数式编程与命令式编程的思路最大的不同在于:函数式更关心数据的映射。

在前端开发领域中,有很多函数式的使用,比如React框架,它本身的设计理念就是View = Fn(Data),而且还有函数式组件以及高阶组件等等,无一不都透露着对函数式编程的实践。


纯函数


纯函数是函数式编程中一个很重要的概念,它的定义是:纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,且不依赖外部环境。

比如说对于数组的方法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);
//=> []
复制代码

可以从例子里看出,对于slice来说,它对于相同的输入总能返回相同的输出;而splice直接在原数组上作出改变,产生了可观察到的副作用,即改变了数组。

再来一个例子(例子来自函数式编程指北):

// 不纯的
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum;
};
// 纯的
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};
复制代码

很明显,在不纯的版本中,checkAge的返回结果直接依赖于外部的变量minimum,这样子就做不到对于相同的输入总是返回相同的输出了,因为只要外部环境的变量一变,直接影响到函数的输出。


副作用


前面一直在说副作用,到底什么是副作用?哪些影响可以被算作是副作用?

副作用的定义是:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用包含但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

可以看出来,只要是跟外部环境发生了交互的行为都会带来副作用,与外部环境交互了就可能会影响到函数的输出,函数式编程之所以这么在乎副作用,就是因为函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。


优势


纯函数因为它的纯和没有副作用,可以带来以下几个好处


可预测性


因为纯函数没有副作用,对于同一个输入来说,我们可以预测到它的输出,这对于前端常见的共享状态以及修改很有用。

再看看Redux,在它的官方文档里面提到:

To specify how the state tree is transformed by actions, you write pure reducers. Reducers are just pure functions that take the previous state and an action, and return the next > state. Remember to return new state objects, instead of mutating the previous state.

很明显,Redux要求你使用纯函数的方式来写reducer,可预测、无副作用的纯函数正中下怀。


可缓存性


为对于纯函数,固定的输入可以得到固定的输出,所以可利用这一点来做缓存。

例如(例子来自函数式编程指北):

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
复制代码


柯里化


柯里化(curry)的理念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

举个例子(例子来自函数式编程指北):

var add = function(x) {
  return function(y) {
    return x + y;
  };
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
复制代码

上面这个例子定义了一个add函数,它接受参数x并且返回一个函数,调用add函数之后,返回的函数就以闭包的形式记住了第一个参数x。

柯里化在日常编程中的使用主要体现在“预加载函数”,即提前缓存一部分参数,以便在未来使用。例如在vue源码中,就有一个柯里化的经典使用:

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    //...
  }
}
复制代码

因为vue需要支持web和weex,而且patch操作里面的逻辑在不同的平台上基本相同,只是把虚拟DOM映射到真实元素的操作方法不一样,所以提前把相关的API传入createPatchFunction,返回一个固化好平台操作元素API的patch方法,以后使用的时候只需要传入oldVnode和vnode等等参数就好了,这种利用柯里化的技巧值得学习。


组合


考虑一下存在如下的方法:

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = function(x){
  return exclaim(toUpperCase(x)); // 不优雅
};
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
复制代码

这种方法里面套方法的做法,看起来略难受且不优雅,如果使用组合的方式,可以修改成下面这样:

var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
复制代码

组合式的写法可以让我们在开发中用拼装来代替直接封装,使代码更加优雅,比如Redux中的compose方法:

Composes functions from right to left.

This is a functional programming utility, and is included in Redux as a convenience. You might want to use it to apply several store enhancers in a row.

Arguments

(arguments): The functions to compose. Each function is expected to accept a single parameter. Its return value will be provided as an argument to the function standing to the left, and so on. The exception is the right-most argument which can accept multiple parameters, as it will provide the signature for the resulting composed function.

Returns

(Function): The final function obtained by composing the given functions from right to left.

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers'
const store = createStore(
  reducer,
  compose(applyMiddleware(thunk), DevTools.instrument())
)
复制代码


没有银弹


本文只是对函数式最基本的一些特性的学习和介绍,以及在前端开发的实践。如果想深入学习函数式编程,比如Monad之类的,可以看一下参考链接,更上一层楼。

软件开发领域没有银弹,没有一种编程范式或者框架放之四海而皆准。对于函数式编程也是如此,取其精华去其糟粕,防止拿着锤子看什么都像是钉子。


参考

函数式编程指南

编程的宗派

什么是函数式编程思维?

前端开发js函数式编程真实用途体现在哪里?

有哪些函数式编程在前端的实践经验?

JavaScript函数式编程

相关文章
|
21天前
|
JavaScript 前端开发 Scala
谈一谈你理解的函数式编程?
谈一谈你理解的函数式编程?
11 0
|
6天前
|
Java 程序员 C#
Lambda表达式:简洁而强大的函数式编程利器
【4月更文挑战第3天】本文探讨了Lambda表达式的基础和在编程中的应用,包括简化代码和提高可读性。Lambda表达式是匿名函数,用于简单的功能,如示例中的平方运算和列表筛选。通过`map`、`filter`等函数,Lambda表达式能有效处理列表操作。文中还展示了Lambda表达式的高级用法,如闭包特性、异常处理及与高阶函数的结合。通过实例,读者可以学习如何利用Lambda表达式实现更高效、简洁的编程。
27 0
|
5月前
|
JavaScript 前端开发 算法
函数式编程
函数式编程
14 0
|
4月前
|
分布式计算 Java API
谈谈代码:函数式编程
一个风和日丽的下午,我看着日常看代码做重构迁移,突然看到这么段代码...
52 1
|
11月前
|
设计模式 前端开发 JavaScript
前端函数式编程浅析
函数式编程的概念与应用
70 0
|
并行计算 JavaScript 数据可视化
快速了解函数式编程
快速了解函数式编程
102 0
快速了解函数式编程
|
前端开发 JavaScript
【我的前端】面向 JavaScript 开发:前端必学的4种函数式编程技术
函数式编程技术是现代 Wed 开发中的热门话题。这一切都是关于将应用程序框架设计为简单功能的组合,一边写更多可扩展的代码。
|
算法 Java Scala
函数式编程基础介绍|学习笔记
快速学习函数式编程基础介绍。
77 0
函数式编程基础介绍|学习笔记
|
Scala 索引 Python
第5章 函数式编程
第5章 函数式编程
435 0
第5章 函数式编程
|
存储 SQL 分布式计算
深入理解函数式编程
深入理解函数式编程
深入理解函数式编程

热门文章

最新文章