在开始了解函数式编程风格以前,咱们须要先思考和了解几个概念。java
引言:如下言论只是我我的的见解一些梳理与记录,若是有任何理解和认知上您认为有问题,很是欢迎在评论区与我一块儿讨论,指出不足之处。编程
什么是纯函数?数组
当咱们建立一个函数,若是这个函数具有如下两个特色:
1) 这个函数指定了输入与输出。而且当调用参数相同时
这个函数永远返回相同的结果,而且不依赖于任何外部状态或数据。
2) 这个函数不会发生任何突变(mutation)或产生任何反作用(effect)。
复制代码
当知足以上两点咱们就称这个函数为【纯函数(Pure Function)】。 换言之,若是使用一个函数时候,不使用他的返回值可是确有意义或做用的话,说明这个函数是【非纯函数】。bash
什么是闭包?为何会产生闭包?如何产生的?闭包
闭包就是一个函数包含着对另外一个函数的引用
复制代码
在建立函数的时候,js 会产生相应的执行环境,在执行环境里会生成活动对象、做用域链等。 执行环境下,js 首先会利用做用域进行变量提高,而后会按顺序进行执行,此时会对变量进行赋值等操做, 执行完毕后会把执行环境从执行环境栈中弹出。 可是因为有可能一个函数包含着对另外一个活动对象的引用 致使被引用的活动对象一直没有被释放,这就是js 闭包产生的缘由。架构
所以因为 js 本质是everything is object,在互相引用的过程当中就会产生闭包。app
由此能够延展:函数式编程
1) ES6 let 声明之因此会产生“暂存死区”(既 let 代码块以上没法使用 let 声明的变量)的现象,
也是因为 let 在执行环境中并无变量提高的过程。
2)ES6 const 没法再次被赋值,也是由于它只有声明阶段,没有赋值阶段。因此 const 声明的变量
也没法再次被赋值。
复制代码
所以闭包具备如下特色:函数
1) 函数嵌套函数
2) 函数内部能够引用外部的参数和变量
3) 参数和变量不会被垃圾回收机制回收
复制代码
什么是高阶函数?测试
当开发初期,咱们使用函数对业务逻辑和运算进行封装,使得函数能够根据咱们的入参进行相应逻辑运算与转换。可是若是当一个函数的参数也是一个函数时,那么这个函数的处理业务的复杂度增长,使得其成为一个高阶函数。咱们能够经过一个例子来观察高阶函数区别于普通函数的特色。
咱们但愿过滤这个数据,找到价格高于 30 元产品,首先咱们先使用一些经常使用的数组操做来完成:
const data = [
{ id: 1, food: "手撕面包", price: 34 },
{ id: 2, food: "牛奶", price: 20 },
{ id: 3, food: "拿铁", price: 26 },
{ id: 4, food: "卡布奇诺", price: 28 },
{ id: 5, food: "馥芮白", price: 40 },
{ id: 6, food: "摩卡", price: 32 },
{ id: 7, food: "耶加雪啡", price: 128 },
];
// 不使用函数式编码
// way1
let one = [];
for (let i = 0, len = data.length; i < len; i++) {
if (data[i].price > 30) {
one.push(data[i].food);
}
}
console.log(one); // [ '手撕面包', '馥芮白', '摩卡', '耶加雪啡' ]
// way2
let two = data.map(item => {
if (item.price > 30) { return item.food }
}).filter(l => typeof l != 'undefined');
console.log(two); // [ '手撕面包', '馥芮白', '摩卡', '耶加雪啡' ]
// way3
let three = data.filter(l => l.price > 30).map( l => l.food);
console.log(three); // [ '手撕面包', '馥芮白', '摩卡', '耶加雪啡' ]
复制代码
上面的 way2, way3 咱们都是在 map 内部直接建立了函数去处理业务。事实上ES6中 map, filter, reduce, some, every 也都是 高阶函数 ,由于他们也是接受一个函数,根据函数执行返回结果,即 const map = list => order => list.map(order)
。
当咱们使用上面的三种方式去过滤数据的时候,能够发现,咱们关注点在于处理什么数据 ?按照什么条件处理?这也是其弊端所在,接下来咱们对条件和数据进行抽象:
// way4
// order 做为条件传入,等待 data 的输入
const select = order => data => data.reduce((prev, next) => {
if (order(next)) prev = [...prev, next];
return prev;
}, []);
/// 业务部分
const condition = data => data.price > 30; // 抽象函数条件
const getDataByCondition = select(condition); // 生成数据获取函数
const getConditionResult = getDataByCondition(data);
console.log(getConditionResult.map(l => l.food));
// [ '手撕面包', '馥芮白', '摩卡', '耶加雪啡' ]
复制代码
对比上面的三种实现,在 reduce 高阶函数参与以后,咱们将条件和数据进行抽象,将过滤函数的关键点都提取了出来,为此咱们创造了一个可以接受其余函数进行逻辑处理的 高阶函数:select。它使得咱们的关注点,从逻辑的判断,转换成了函数的编写与合理化的命名。多种函数互相组合,互相赋能,这也是高阶函数的魅力所在。
但仔细观察上面的实现(way4),彷佛也存在问题,reduce 做为一个高阶函数,应该能够进一步抽象,来应对跟多场景,所以咱们能够进一步拓展:
// way 4 改版
// 抽象 reducer 内部函数
const select = order => data => data.reduce(order, []);
// 定义 条件生成函数
const conditionHandler = condition => (prev, next) => {
if (condition(next)) prev = [...prev, next];
return prev;
}
// 业务部分
const condition = item => item.price > 30; // 定义条件
const filterByPrice = conditionHandler(condition); // 生成过滤函数
const getDataByCondition = select(filterByPrice); // 生成数据处理函数
console.log(getConditionResult.map(l => l.food));
// [ '手撕面包', '馥芮白', '摩卡', '耶加雪啡' ]
const condition = item => item.food === "摩卡"; // 修改条件
console.log(getConditionResult.map(l => l.food)); // [ '摩卡' ]
复制代码
到这里,咱们针对筛选这个业务场景,抽象了两个可复用的高阶函数,select 和 conditionHandler。我在使用这两个函数去处理筛选业务时,个人关注点在于如何合理化命名函数,理解业务并建立对应的条件函数。由此咱们发现 函数式编程 的基本思想就是这种高度抽象的编程规范。而 高阶函数 则是在这种编程思惟下所使用的一种编码方式而已。
经过上面的例子,我已经能理解 高阶函数 其实就是接受其余函数做为入参的一种函数而已。
知道了闭包和高阶函数,那么什么是函数的柯里化呢 ?
以前对柯里化有必定了解的朋友必定知道柯里化函数的特色或者做用:
1) 参数可复用
2) 提早确认
3) 延迟运行
复制代码
咱们经过一个简单的例子来理解这三个特征: 如何实现一个加法函数,使其能够接受任意个参数和组合形式进行加法运算,即
add(1)(2)(3) // 6
add(1, 2, 3) // 6
add(1, 2)(3) // 6
代码实现:
function curry(fn, scope, ...args) {
let
len = fn.length, // 拿到 函数 的 参数长度
prex = args, // 保存上一次的 prex
context = scope; // 保存做用域
let newFn = (...rest) => {
let last = prex.slice(0).concat(rest); // 合并入参
if (last.length < len) {
return curry.call(this, fn, context, ...last); // 继续柯里化
} else {
return fn.call(context, ...last); // 获执行函数
}
}
return newFn;
}
function add(a, b, c) {
return a + b + c;
}
let curryAdd = curry(add, add);
console.log(curryAdd(1)(2)(3)); // 6
console.log(curryAdd(1, 2, 3)); // 6
console.log(curryAdd(1, 2)(3)); // 6
复制代码
上面的代码我加了注释,能够看到,经过curry 高阶函数 ,当传入参数不足的时候,咱们利用闭包的特色,保存以前入参,而且返回一个新的函数来继续等待接收参数,以达到 参数复用 和 延时执行 的目的。能够看到函数通过柯里化以后,对简单的 a + b + c 这个过程进行了抽象,为这个简单的 + 法操做进行赋能,让你能够控制每个变量并根据不一样的状况进行函数的简单函数的复杂抽象来应对更多的状况,从而达到 提早确认 的特色。
由此对于函数的柯里化,咱们已经有所领悟。即 柯里化(Currying)就是将须要多个参数的函数转换为一个函数的过程,当提供较少的参数时,返回一个等待剩余参数的新函数。这就是函数的柯里化。
上面的加法还能够继续拓展,若是想支持无限个参数进行加法,应该怎么作呢?
add(1,2)(3)(1)(2) // 9
add(1)(1)(1)(1)(1)...(n) // n * 1
咱们须要对上面的 curry 和 add 函数进行改造,来应对这种需求
function curry(fn, scope, ...args) {
let
prex = args, // 保存上一次的 参数
context = scope; // 保存做用域
let newFn = (...rest) => {
let last = [...prex, ...rest]; // 合并入参
return ( // 当没有入参时,执行函数,不然继续柯里化函数
rest.length > 0 ?
curry(fn, context, ...last) :
fn.call(context, ...last)
)
}
return newFn;
}
function add(...args) {
return args.reduce((last, next) => last + next, 0);
}
var curryAdd = curry(add);
console.log(curryAdd(1, 2, 3)()); // 6
console.log(curryAdd(1, 2)(3)()); // 6
console.log(curryAdd(1)(1)(1)(1)(1)()); // 5
console.log(curryAdd(1, 1)(1, 1)()); // 4
复制代码
这样就实现了多参版本的 add 方法。须要关注的是,在实现多参版本时,咱们对 add 函数进行改造,咱们使用了 reduce 这个高阶函数 ,高阶函数和柯里化的结合,使得咱们没必要关注函数接受的每一个参数,而专一于为函数进行赋能。这也是函数式编程的核心所在(专一于IO);
当咱们了解柯里化以后,咱们很容易理解 bind 函数(绑定指针,返回一个等待执行的函数) 的实现与原理了:
// 使用: fn.bind(this, ...args); | Function.prototype.bind = fn....
Function.prototype.bindFn = function (context) {
let bindedFn = this; // 拿到 bind 的函数
let args = Array.prototype.slice.call(arguments, 1); // 去除构造函数,拿到入参
let pendingFn = function (...rest) {
let last = [...args, rest];
return bindedFn.apply(context, ...last);
}
return pendingFn; // 返回一个待执行的函数,接受新的参数
}
复制代码
waning: 上面的代码只是对 Bind 原理的简单实现,没有考虑 bind 方法使用 new 建立的状况。
到目前为止,咱们在回顾一下到底什么是函数的柯里化?相信咱们能够更好的理解柯里化(Currying)就是将须要多个参数的函数转换为一个函数的过程,当提供较少的参数时,返回一个等待剩余参数的新函数。
这个定义了。
函数式编程?声明式编程?命令式编程?
到这里咱们已经了解了 纯函数 ,闭包,高阶函数 以及函数的 柯里化 的定义、本质,以及他们之间的关系;当咱们看透了本质,针对某种业务场景进行抽象,或是但愿编写出可复用、易测试、易维护的代码时,咱们可能考虑高阶组件,高阶函数的使用。那么就可能须要在编程风格和架构设计上作出改变。
声明式编程与命令式编程的区别
蔬菜(类) => 作成菜(方法), 接受入参(各类菜)
蔬菜.作成菜(牛油果,各类蔬菜); // 沙拉
蔬菜.作成菜(胡萝卜,青菜,油); // 炒蔬菜
复制代码
声明式编程关注点在于 “咱们须要获得什么” ,这种编程方式只在意作什么、要获得什么,抽象了实现细节,而 命令式编程 实现这种过程则更像是
洗干净(蔬菜)
混合(蔬菜,沙拉)
放入盘中(混合物)
复制代码
能够发现 命令式编程 关注点在于 “咱们如何去作”,更加在意计算机执行的步骤,一步一步告诉计算机先干什么,再干什么。把细节按照人类的思想以代码的形式表现出来。这也是为何命令式编程将直接致使代码难以维护、难以测试、难以复用的缘由(部分业务场景)。
函数式编程与声明式编程的关系
在 javascirpt 中,咱们将 函数 做为参数进行传递,创造复杂度更高,功能更增强大的函数。咱们进行函数式编程,把函数做为 “一等公民”。经过纯函数, 递归, 内聚, 惰性计算, 映射等进行组合,使得代码在实现 “要获得什么” 这个过程当中,获取更强大的抽象与计算能力。所以 函数式编程 是 声明式编程 的一部分。
总结
当一个函数指定输入后,输出永远相同,而且没有任何突变及反作用,那么这个函数就是一个纯函数
闭包就是一个函数包含着对另外一个函数的引用
接受其余函数做为入参的一种高级函数
将须要多个参数的函数转换为一个函数的过程,当提供较少的参数时,返回一个等待剩余参数的新函数
命令式编程关注实现细节,声明式编程关注实现结果,弱化并抽象细节。函数式编程是声明式编程的一部分。