3万6千字爆肝,前端进阶不得不了解的函数式编程开发,含大量实例,手写案例,所有案例均可运行
Posted GoldenaArcher
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3万6千字爆肝,前端进阶不得不了解的函数式编程开发,含大量实例,手写案例,所有案例均可运行相关的知识,希望对你有一定的参考价值。
3w6爆肝,前端进阶不得不了解的函数式编程开发,含大量实例,手写案例,所有案例均可运行
还不了解React所提倡的,Vue最近也慢慢兼容的函数式编程风格吗?那看看这里,深入理解一下函数式编程及其好处,写出更加优雅的代码吧。
本章内容包含:
理解函数式编程,如为什么要学习函数式编程,函数式编程所带来的好处,高阶函数的意义,以及手写实现一些常见的 Array 函数
柯里化,包括概念讲解、案例以及手写实现柯里化
组合函数,组合函数的讲解和理解,以及手写函数组合的处理方法
Lodash 中 fp 的使用,以及 fp 和 lodash 的一些对比
Point-free 的概念和相关案例
包括 Function 函子,MayBe 函子,Either 函子,IO 函子, of 函子, Monad 函子
以上内容中都或多或少地带了一些 Lodash 的使用
前前后后总共学了差不多一周的时间,笔记零零碎碎地写了两三天。除了视频的内容之外,也结合了一些自己的开发经验补充了一些案例,希望能够共同进步,早日上岸 w
认识函数式编程
函数式编程(Functional Programming) 是一种编程思想,起源于 λ-calculus(Lambda Calculus),在 1930 年的时候就已经被用于功能性应用、定义及递归上。其核心思维就是抽象化过程,关注输入和输出。
应用函数式编程思想的编程语言家族成员有:
- LISP,一个 1960 年就被设计出来的编程语言,至今仍然非常活跃
- Meta Language(ML)
- Scala
- F#
- Erlang
- Haskell
- …
最近随着 React 的流行,函数式编程的思想又再一次流行起来了。
它的特点有以下几个:
即:
-
不可变数据(Immutable Data)
React 和 Redux 的核心思维,Vue 也引进了这个概念
-
闭包(Closure)
javascript 中应用广泛的痛点之一
-
函数是一等公民(First-class function)
函数式编程是一种概念,既然早在编程语言出现之前就有了这种思想,那么这个函数指代的自然不会是编程语言中的函数。
它所指代的是更纯粹的数学中的函数映射关系,如 y = s i n ( x ) y = sin(x) y=sin(x) 中, x x x 和 y y y 的关系
-
维护性(Maintainability)
依赖于其纯函数的特性,即 相同的输入始终会得到相同的输出,这也是数学中幂等性的概念
-
模块化(Modularity)
三大主流框架之中 Angular 不了解,只知道是 MVC 的结构;但是 React 和 Vue 都在走模块化拆分的路线
这么一算就是大半的主流前端框架都在使用模块化的思维进行开发,自然可以说明函数式开发有其自己的优点
-
引用透明性(Referential Transparency)
即函数的返回值只依赖其输入值的思想,这种思想可以使得代码更加模块化和易于测试。
这也代表着函数式编程是用来描述 数据(函数) 之间的映射(关系)
从应用层的角度上来说,函数式编程也有以下几个优点:
-
因为函数的返回值只依赖于其输入值,所以可以抛弃对 this 的依赖
-
可以更好地利用 tree shaking 过滤无用代码
即将不相关的代码「摇」掉,在打包的过程中不将不想关的代码打包的做法
-
方便测试
基于其幂等性,所以每一次给予相同的输入都应该会返回相应的输出。因为可预测性,也就使得测试变得更加简单
-
众多的库可以帮助开发
最常有的有 lodash,还有 underscore, rambda
综上所述,这也是为什么对于前端人员来说,学习和了解函数式编程开发俨然成了一个必修课。
函数相关复习
函数是一等公民
当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有 头等函数
这是来自 MDN 的定义,MDN 将 first-class function 翻译成了头等函数,本质上是一个东西。
在 JavaScript 中,除了原始值之外的其他值都是对象,包括函数,也因此,函数就可以被存储到变量/数组中去,也可以作为另外一个函数的参数和返回值。
函数作为变量在 JavaScript 是一个比较常见的写法了,也被称之为 函数表达式:
const fn = function () {
console.log('Function expression');
};
fn();
// 更加复杂的应用法
const BlogController = {
index(posts) {
return Views.index(posts);
},
show(post) {
return Views.show(posts);
},
create(attr) {
return DB.create(attrs);
},
update(post, attrs) {
return DB.update(post, attrs);
},
destroy(post) {
return DB.destroy(post);
},
};
// 优化
// 将方法作为值 赋给另一个方法
// 注意,赋的是方法本身,而不是其调用,所以 () 在这里是不需要的
const BlogController = {
index: Views.index,
show: Views.show,
create: DB.create,
update: DB.update,
destroy: DB.destroy,
};
上面的案例已经充分说明了 函数被当作变量一样用 的特性,而这个特性,就是高阶函数和柯里化的基础。
高级函数
高阶函数(Higher-order Function)有以下两个特性:
-
函数可以作为参数
这个特性 React 里面的 HOC 用的贼 6
-
函数可以作为返回值
函数作为参数
这个特点依旧是围绕函数的模块化来实行的,即将函数封装为一个模块。
以 y = f ( x ) y = f(x) y=f(x) 为例,对于 y y y 而说, f ( x ) f(x) f(x) 的具体实现过程是被抽象化的, y y y 只需要得到 f ( x ) f(x) f(x) 返回的结果,并且 f ( x ) f(x) f(x) 返回的结果是正确的即可。
注:函数名必须要有意义,需要自证其用。
案例 1,模拟 forEach
对于 forEach()
来说,这个函数的意义就是 对数组的每个元素执行一次给定的函数。至于函数内部是怎么完成数组的遍历的,调用者并不在乎。
// 接受一个函数去在遍历中执行
/**
* 对数组的每个元素执行一次给定的函数
*
* @param {arr} arr
* @param {func} fn
*/
function forEach(arr, fn) {
for (let i = 0; i < arr.length; i++) {
fn(arr[i]);
}
}
// 测试部分
const arr = [1, 2, 3, 4];
forEach(arr, function (item) {
console.log(item);
});
// 输出为
// 1
// 2
// 3
// 4
案例 2,模拟 filter
同样的,对于 filter()
来说,这个函数的意义就是 新建一个新数组, 其包含通过所提供函数实现的测试的所有元素。至于函数内部是怎么完成数组的遍历的,调用者同样不在乎。
/**
* 新建一个新数组, 其包含通过所提供函数实现的测试的所有元素
* 为了通用性,接受一个函数去在遍历中执行
* @param {array} arr
* @param {fn} fn
* @returns
*/
function filter(arr, fn) {
const results = [];
for (let i = 0; i < arr.length; i++) {
const el = arr[i];
if (fn(el)) {
results.push(el);
}
}
return results;
}
// 测试部分
const arr2 = [10, 15, 20, 25];
const result = filter(arr2, function (item) {
return item % 2 === 0;
}); // 结果为 [ 10, 20 ]
函数作为返回值
函数作为返回值就等于让一个函数生成另一个函数。
函数作为返回值使用案例
下面是一个初级案例,讲的是怎么使用函数去生成另一个函数:
/**
* 用函数生成另外一个函数
* @returns function
*/
function generateFn() {
let msg = 'Hello World';
return function () {
console.log(msg);
};
}
const fn = generateFn();
fn(); // Hello World
// 第二种调用方式
generateFn()(); // Hello World
once 案例
知道了怎么生成函数,接下来就要写一个有意义的案例了。
lodash 中的 once 就是只让函数执行一次,这种其实我觉得使用最多的应该是单例模式了,创建一个变量,然后让全局引用。
使用案例如下:
/**
* 让一个函数只执行一次
* @param {function} fn
*/
function once(fn) {
let done = false;
return function () {
if (!done) {
done = true;
// arguments 是匿名函数中的 artuments
return fn.apply(this, arguments);
}
};
}
const pay = once(function (amount) {
console.log(`Has paid: $${amount}`);
});
pay(100); // 只会执行一次
pay(100); // 运行到这里的时候,done就已经是false了,所以后面不会再被执行
pay(100);
pay(100);
pay(100);
高阶函数的意义
有两点:
- 对运算过程进行抽象处理通用的问题
- 屏蔽掉抽象的部分,只需要关注目标,即运行结果
这也是函数式编程的核心思想,如上面写的 forEach
和 filter
,就是抽象化了内部的实现过程,只关注其运行结果——对 forEach
来说是遍历,对 filter
是过滤。
常用的高阶函数
模拟了一部分部分数组的通用函数,鉴于都已经知道这些函数的目的了,注释就偷下懒了……
map
直接写实现了。
const map = (arr, fn) => {
const results = [];
for (let el of arr) {
results.push(fn(el));
}
return results;
};
const arr = [1, 2, 3, 4];
console.log(map(arr, (el) => el ** 2)); // [ 1, 4, 9, 16 ]
array
直接写实现了。
const arr = [1, 2, 3, 4];
const every = (arr, fn) => {
for (const el of arr) {
if (!fn(el)) {
return false;
}
}
return true;
};
console.log(every(arr, (el) => el > 2)); // false
console.log(every(arr, (el) => el > 0)); // true
some
直接写实现了。
const arr = [1, 2, 3, 4];
const some = (arr, fn) => {
for (const el of arr) {
if (fn(el)) {
return true;
}
}
return false;
};
console.log(some(arr, (el) => el > 2)); // true
console.log(some(arr, (el) => el > 5)); // false
闭包
JavaScript 最大的痛点之一……
闭包的概念
闭包(closure) - 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
翻译:可以在另一个作用域中调用一个函数的内部函数,并且访问到该内部函数的作用域中的成员
理解一下,就是,JavaScript 是原型链继承的,所以正常的使用都是下克上——原型链底层调用上层。使用闭包完成了上克下——即调用原型链下层的变量。
以之前的案例来说,generateFn
和 once
都是使用了闭包的概念。例如说他们的调用都是在全局作用域中,可是在全局作用域中,它们能够访问 generateFn
和 once
中的变量。
以 generateFn
为例:
当闭包不存在的时候,因为 ES6 的新特性就是在 {}
之中新构筑了一个作用域,所以当 generateFn
执行完毕后,msg
的引用不存在了,从而被垃圾回收。
function generateFn() {
let msg = 'Hello World';
} // 离开这个作用域之后,msg的引用就消失了
const fn = generateFn();
fn();
但是,当闭包存在滞后,情况就不一样了:
function generateFn() {
let msg = 'Hello World';
return function () {
// 对 msg 还存在引用关系
console.log(msg);
};
} // 匿名函数被作为引用对象返回给了外层的对象,引用依然存在,msg无法被销毁
// 生成了对 generateFn() 中的匿名函数的引用
// 但凡 fn 没有被销毁,generateFn() 中的 msg 就 无法被释放
const fn = generateFn();
案例
案例假设:经常会调用数字的幂,为了减少变量的传输,因此会将 Math.pow(val, nthPow)
进行封装。
function generateNthPow(pow) {
return function (num) {
return Math.pow(num, pow);
};
}
const square = generateNthPow(2);
console.log(square(2));
接下来分析一下上面这个案例的执行过程。
-
当执行到
const square = generateNthPow(2);
时:调用
generateNthPow(pow)
,并且返回 其中的匿名函数此时
square
的值为function (num) {...}
的引用 -
当执行到
console.log(square(2))
时调用其中的匿名函数,并且返回
Math.pow(num, pow)
的值,注意一下现在的作用域:现在的局部作用于是匿名函数的作用域,因此能够看到
num: 2
,而下面的闭包(closure),就是跟闭包相关的变量。再回忆一下现在的函数的声明时在
global
这个作用域之中,它访问到了一个generateNthPow
的内部作用域中的内部函数,并且还能够访问到generateNthPow
中的的变量,也就是pow
。
适当地使用闭包可以减少函数的重复性,从而减少代码量,写出更加优雅的代码。
函数式编程基础
lodash
一个函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法。
这里要重点注意 lodash/fp
,这是一个 FP 样式友好的模块,它不会长生副作用。
lodash 的一些功能展示如下:
// 演示 lodash
// first / last / toUpper / reverse / each / includes / find / findIndex
const _ = require('lodash');
const array = ['jack', 'rose', 'tom', 'jerry'];
console.log(_.first(array)); // jack
console.log(_.last(array)); // jerry
console.log(_.toUpper(array)); // JACK,ROSE,TOM,JERRY
// 会改变 array 的顺序
console.log(_.reverse(array)); // [ 'jerry', 'tom', 'rose', 'jack' ]
_.each(array, (item, index) => {
console.log(item, index);
});
// jerry 0
// tom 1
// rose 2
// jack 3
// includes, find, findIndex 是 ES6 之后新加的函数
// lodash的对应函数作用与ES6的函数是一样的
console.log(_.includes(array, 'jack')); // true
console.log(_.find(array, (el) => el === 'tom')); // tom
console.log(_.findIndex(array, (el) => el === 'rose')); // 2
效果截图:
纯函数
纯函数的概念和特点
纯函数的概念和特点有以下三点:
-
相同的输入永远会得到相同的输出,并且没有任何可观察的副作用。即,保持了函数的幂等性.
纯函数就像是数学中的函数,可以用 y = f ( x ) y = f(x) y=f(x) 来描述 输入 x x x 与 输出 y y y 之间的关系。
数组中
slice
和splice
分别对应了纯函数和不纯函数:-
slice
: 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括 end)。原始数组不会被改变。 -
splice
: 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
let array = [1, 2, 3, 4, 5]; console.log(array.slice(0, 3)); // [ 1, 2, 3 ] console.log(array.slice(0, 3)); // [ 1, 2, 3 ] console.log(array.slice(0, 3)); // [ 1, 2, 3 ] // 结果输出相同,证明slice是纯函数 console.log(array.splice(0, 3)); // [ 1, 2, 3 ] console.log(array.splice(0, 3)); // [ 4, 5 ] console.log(array.splice(0, 3)); // [] // 结果输出改变,证明splice不是纯数组
-
-
函数式编程不会保留计算中间的结果,所以状态是不可变的(无状态的)
-
刻意把一个函数的执行结果交给另一个函数去处理(高阶函数)
好处
-
可缓存
因为纯函数对相同的输入始终有相同的结果,所以可以吧纯函数的结果缓存起来
如:若是一个函数执行起来非常耗时,并且又是一个纯函数,那么再知道给予相同的输入会有相同的输出的前提条件下,就可以将这个函数结果存在一个变量里面,等到下次调用时使用。
以上是关于3万6千字爆肝,前端进阶不得不了解的函数式编程开发,含大量实例,手写案例,所有案例均可运行的主要内容,如果未能解决你的问题,请参考以下文章