目录
一. 函数式编程范式
1.1 编程范式定义
- 函数式编程(Functional Programming: FP)是一种 编程范式(指计算机编程中的典范模式或方法,就是一种思维方式),属于结构化编程,用于描述数据(函数)之间的映射关系
- 注意:函数式编程中的函数,不是指程序中的函数(方法),而是数学中的函数(映射关系),如:y = f(x),指 x 和 y 之间的关系
- 常见的编程范式有:过程化(命令式)编程、面向对象编程、声明式编程等
过程化编程:最原始的传统编程,将问题抽象为一系列步骤,然后通过编程方式将这些步骤转换为程序指令集,这些指令集按照一定顺序排列;常见的过程化编程语言有机器语言、汇编语言、BASIC、C、FORTRAN 等;过程化语言特别适合解决线性(或者说按部就班)的算法问题。- 面向对象编程:将待解决问题抽象为面向对象的程序中的对象,利用封装使每个对象都拥有个体的身份;程序就是成堆的对象,彼此通过信息的传递,请求其它对象进行工作;面向对象包括三个基本概念:封装性、继承性、多态性。常见的面向对象语言有 Java、C、C++、JavaScript。
- 声明式编程:以数据结构的形式来表达程序执行的逻辑。只需要定义好该如何处理数据,不需要指定具体实现;SQL 语句就是最明显的一种声明式编程的例子。
现代编程语言的发展趋势:支持多种范式,如 C#、Java 8+、Kotlin、ES6+
编程范式 vs 设计模式
- 编程范式:是指从事软件工程的一类典型的编程风格(此概念好比 “战略” ),体现编写程序的人如何看待程序设计的 “哲学观”
- 设计模式:设计模式是软件设计中常见问题的典型解决方案(此概念好比 “战术” ),是解决一系列实际问题的 “方法学”
1.2 函数式编程范式特点
- 代码简洁,函数式编程使用了大量的函数,减少了代码的重复;
- 接近自然语言,易于理解;
- 函数是 “第一等公民”:函数与其他数据类型一样,可以赋值给其它变量,也可以作为参数,也可以作为返回值;
- 函数式编程会使用较多的 闭包 和 高阶函数;
- 没有 “副作用”,方便与代码管理和单元测试:副作用指函数内部与外部互动(比如意外修改全局变量),这意味着函数要保持独立,所有功能就是返回一个新的值,没有其他⾏为,尤其是不得修改外部变量的值;
- 引用透明:函数的运行不依赖于外部变量或 ”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数的返回值相同
let result = (1 + 2)* 3 - 4; // 普通表达式 let result = subtract(multiply(add(1, 2), 3), 4); // 函数式编程
1.3 函数式编程范式基本概念
- 函数式编程的思维方式?
- 把现实世界的事物和事物之间的联系(映射关系)抽象到程序世界(对运算过程进行抽象)
- 函数式编程是用来描述数据(函数)之间的映射
// 比如买单价为0.5元的白菜,买了两斤,需要支付多少块钱(白菜与货币的联系) let money = multiply(0.5, 2); // 即两斤白菜 -> 1元(money)
程序的本质?- 根据输入,通过某种运算获得相应的输出
y = f(x) 函数关系?- x → f(映射) → y,x 与 y 是一一对应的
- 映射(函数)关系【一一对应】:
- 非映射(函数)关系【非一一对应】:
何谓纯函数?- 相同输入 始终要得到 相同输出
1.4 章节学习指南
- 函数式编程范式,是一种对 程序编程思维 的一种概论,函数式编程范式的 具体实现 是通过柯里化(第 5 章)、函数组合(第 6 章)、函子等完成的
- 在学习如何实现函数式编程范式前,需要先了解三个小知识点:头等函数(第 2 章),闭包(第 3 章),纯函数(第 4 章)
二. 头等函数
2.1 一等公民 和 高阶函数
- 为什么说 函数是一等公民呢?因为在某些编程语言中,函数不能实现:
- 函数可以存储在变量中
- 函数可以作为参数(2.2.1)
- 函数可以作为返回值(2.2.2)
- JavaScript 中,函数可以享受以上几种待遇,所以函数是一等公民
何谓高阶函数?- 可以操作函数的函数,被称为 高阶函数,有两种情况:
- 参数是一个函数
- 返回值是一个函数
2.2 函数作为参数
- 将函数作为参数,最常见的应用是:各种回调函数
// 遍历(模拟数组的 forEach 方法) function forEach(arr, fn) { for (let i = 0; i < arr.length; i++) { fn(arr[i], i); // 将每一项传入回调 fn 处理 } } // 筛选,返回符合条件的元素组成的新数组 function filter(arr, fn) { const results = []; for (const item of arr) { if (fn(item)) { results.push(item); } } return results; } // 遍历(模拟数组的 forEach 方法) - 举例 const colors = ["#FF0000", "#00FF00", "blue"]; forEach(colors, (item, index) => { console.log(index + 1, item); }); // 筛选,返回符合条件的元素组成的新数组 - 举例 console.log(filter(colors, (item) => item.length === 7));
- 体现出的高阶函数意义:高阶函数用来抽象 通用 的问题,这里抽象了遍历的逻辑
2.3 函数作为返回值
function makeFn() { const msg = "hello function"; return function () { console.log(msg); }; } // makeFn() 执行后返回一个匿名函数,赋值给 fn const fn = makeFn(); fn(); // 等价于 makeFn()() // 模拟 lodash 中的 once函数,让函数仅执行一次 // 举个栗子:支付,不管用户点击多少次按钮,都只执行一次 function once(func) { let done = false; // 定义一个状态 done,判断是否已执行支付 return function () { if (!done) { done = true; // 更改闭包作用域中的 done 为已支付 func.apply(this, arguments); } }; } const pay = once((money) => { // 传入一个函数,通过输出模拟支付过程和结果 console.log(`支付${money}元`); }); pay(20); // 支付20元 pay(30); pay(40);
- 体现出的高阶函数意义:高阶函数用来屏蔽细节,只关注目标;
- 比如不用在乎怎么遍历,只需要关注遍历后怎么处理数据;
- 比如不用在乎用户是否会多次点击,只需要关注支付后的操作;
2.4 常用高阶函数模拟
2.4.1 map
- map() 函数:返回被传入函数处理后的数组
function map(arr, fn) { const res = []; for (const val of arr) { res.push(fn(val)); // 将回调fn()处理好的元素存入新数组 } return res; } let arr = [1, 2, 3, 4, 5]; arr = map(arr, (item) => item * item); console.log(arr); // [ 1, 4, 9, 16, 25 ]
2.4.2 every
- every() 函数:检测数组中的所有元素,是否每个都符合指定条件
function every(arr, fn) { let res = true; // 定义一个flag for (const val of arr) { res = fn(val); // fn判断 if (!res) { // 只要有一个元素不满足,就结束循环 break; } } return res; } const arr1 = [1, 2, 3, 4, 5]; const arr2 = [4, 5, 6, 7]; const res1 = every(arr1, (item) => item > 3); console.log(res1); // false const res2 = every(arr2, (item) => item > 3); console.log(res2); // true
2.4.3 some
- some() 函数:检测数组中的所有元素,是否至少有一个元素满足条件
function some(arr, fn) { let res = false; // 定义一个flag for (const val of arr) { res = fn(val); // fn判断 if (res) { // 只要有一个元素满足,就结束循环 break; } } return res; } const arr1 = [1, 2, 3, 4, 5]; const arr2 = [1, 3, 5, 7]; const res1 = some(arr1, (item) => item % 2 === 0); console.log(res1); // true const res2 = some(arr2, (item) => item % 2 === 0); console.log(res2); // false
2.4.4 find
- find() 函数:返回数组中 满足传入函数的 第一个元素的值;如果未找到,则返回 undefined
function find(arr, fn) { for (const item of arr) { if (fn(item)) { // 找到满足条件的第一个元素 return item; } } return undefined; // 未找到返回undefined } const arr1 = [1, 2, 3, 4, 5]; const res1 = find(arr1, (item) => item % 2 === 0); console.log(res1); // 2 const res2 = find(arr1, (item) => item === 8); console.log(res2); // undefined
2.4.5 findIndex
- findIndex() 函数:找到满足条件的第一个元素,返回其位置;如果未找到,则返回 -1
function findIndex(arr, fn) { for (let i = 0; i < arr.length; i++) { if (fn(arr[i])) { // 找到满足条件的第一个元素位置 return i; } } return -1; // 未找到返回-1 } const arr1 = [1, 2, 3, 4, 5]; const res1 = findIndex(arr1, (item) => item % 2 === 0); console.log(res1); // 1 const res2 = findIndex(arr1, (item) => item === 8); console.log(res2); // -1
三. 闭包
3.1 闭包定义及特性
- 闭包的本质:
- 函数执行时,会被放到执行栈上,函数执行完毕后,会从执行栈上删除,但是堆上作用域成员因为被外部引用而不能被释放,因此内部函数依然可以访问到作用域的成员;
闭包的特性:
- 函数嵌套函数(高阶函数)
- 函数内部可以引用函数外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
3.2 闭包 Demo
function makePower(power) { return function (number) { return number ** power; // number为底数,power为指数 }; } // 平方:number**2 const power2 = makePower(2); // 立方:number**3 const power3 = makePower(3); console.log(power2(5)); console.log(power3(4)); =============================================================== function makeSalary(base) { return function (performance) { return base + performance; }; } // 底层打工人 const level1 = makeSalary(1000); // 高级打工人 const level2 = makeSalary(10000); console.log(level1(100)); // 1100 console.log(level2(30000)); // 40000
- 以 第二个 Demo 为例:
- level2 是 makeSalary(10000) 返回的一个函数,也就是:function (performance) { return 10000 + performance }
- level2(30000) 也就相当于:function (30000) { return 10000 + 30000 }
3.3 Chrome 开发者工具 > Sources
- Call Stack(函数调用栈)
- Scope(作用域) : Global(var 全局) 、 Local(局部) 、 Closure(闭包) 、 Script(let 作用域)
四. 纯函数
4.1 何谓纯函数?
- 相同的输入 永远会得到 相同的输出,没有任何副作用
举个栗子:- slice 和 splice 分别:纯函数和不纯函数
- slice 返回数组中的指定部分,不会改变原数组
- splice 对数组进行操作返回该数组,会改变原数组
// 纯函数 slice(start, end) const numbers = [1, 2, 3, 4, 5]; console.log(numbers.slice(0, 3)); // [ 1, 2, 3 ] console.log(numbers.slice(0, 3)); // [ 1, 2, 3 ] console.log(numbers.slice(0, 3)); // [ 1, 2, 3 ] // 不纯函数 splice(index, howmany, ...items) console.log(numbers.splice(0, 3)); // [ 1, 2, 3 ] console.log(numbers.splice(0, 3)); // [ 4, 5 ] console.log(numbers.splice(0, 3)); // [] // 最简单的纯函数示例 function getSum(a, b) { return a + b; } console.log(getSum(1, 2)); // 3 console.log(getSum(1, 2)); // 3 console.log(getSum(1, 2)); // 3
4.2 纯函数库 Lodash
- 官方网站:Lodash
- Lodash 是一个纯函数的功能库,它提供了对数组、数字、对象、字符串、函数等操作的很多方法
const _ = require("lodash"); const arr = ["Tom", "Jon", "Kate"]; console.log(_.first(arr)); console.log(_.toUpper(_.last(arr))); console.log(_.reverse(arr)); _.each(arr, (item, index) => { console.log(item, index); }); const value = []; _.isEmpty(value); // 判断一个value 是否是 empty(null, [], {}....)
4.3 纯函数的好处
- 可缓存:因为纯函数相同的输入永远会等到相同的输出,所以可以把纯函数结果缓存【比如 Lodash.memoize(func)】
- 可测试:纯函数让测试更加方便,对单元化测试很友好
- 并行处理:在多线程环境下,并行操作共享的内存数据,很可能会出现意外情况;纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)
关于纯函数的好处 —— “可缓存” 的栗子:// lodash 记忆函数 const _ = require("lodash"); function getArea(r) { console.log(`执行getArea计算,r = ${r}`); return Math.PI * r * r; } // 这里使用lodash中的记忆函数 const getAreaWithMemory = _.memoize(getArea); console.log(getAreaWithMemory(4)); console.log(getAreaWithMemory(4)); // 不会再次计算 console.log(getAreaWithMemory(5)); =========================================================== // js模拟 memoize 方法的实现 function memoize(f) { const cache = {}; return function () { const key = JSON.stringify(arguments); cache[key] = cache[key] || f.apply(f, arguments); return cache[key]; }; } const getAreaWithMemory = memoize(getArea); console.log(getAreaWithMemory(4)); console.log(getAreaWithMemory(4)); console.log(getAreaWithMemory(5));
4.4 关于 “副作用”
- 纯函数:指 相同的输入永远会得到相同的输出,而且没有可观察的 副作用
- 由概念得:副作用让函数变的不纯,纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态,就无法保证输出相同,也就会带来副作用
- 举个栗子:
// 不纯的函数 let min = 18; function checkAge(age) { return age >= min; // 依赖外部的 min 状态 } // 纯函数 function checkAge2(age) { let min = 18; // 硬编码,可通过闭包或者柯里化解决 return age >= min; }
副作用的来源:配置文件、数据库、获取用户的输入- 所有的外部交互都有可能带来副作用;
副作用会使方法通用性下降、不适合扩展;- 副作用不可能完全禁止,尽可能控制他们在可控范围内发生;
// 有副作用 let result = 0; function sum() { const a = $(".input-1").val(); const b = $(".input-2").val(); result = a + b; } // <button onclick="sum()">求和</button> ========================================================== // 避免副作用 function sum(a, b) { return a + b; } $("button").bind("click", () => { const a = $(".input-1").val(); const b = $(".input-2").val(); result = sum(a, b); }); // <button>求和</button>
五. 柯里化
5.1 何谓柯里化?
- 柯里化(curry:咖喱)把 多元函数 转化成 一元函数
- 解释:当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接受剩余的参数,直到参数接收完毕,才返回结果
- 举个栗子:
// 闭包的方式解决(简单的柯里化) function checkAge(min) { return function (age) { return age >= min; }; } // es6写法 let checkAge = (min) => (age) => age >= min;
柯里化的好处:
- 可以实现 给函数传递较少的参数,得到一个已经记住某些固定参数的新函数
- 这是一种对函数参数的 “缓存”(闭包)
- 让函数变的更灵活,让函数的粒度更小
- 可以把 多元函数 转换成 一元函数,可以组合使用函数,产生强大的功能
5.2 Lodash.curry(func)
- 文档:lodash.curry | Lodash 中文文档 | Lodash 中文网
- 功能:创建一个函数,该函数接受 func 的参数。如果 fn 所需的参数都被提供了,则执行 func 并返回结果,否则 继续返回该函数,并等待接收剩余的参数【类似于 5.1 中,柯里化的解释】
- 注意:传参先后顺序不能变
- 举个栗子:
const _ = require("lodash"); function getSum(a, b, c) { return a + b + c; } const curried = _.curry(getSum); console.log(curried(2, 3, 4)); // 9 console.log(curried(2)(3)(4)); // 9 console.log(curried(2)(3, 4)); // 9 console.log(curried(2, 3)(4)); // 9
- 再举个栗子:
const _ = require("lodash"); const match = _.curry((reg, str) => { return str.match(reg); }); // 匹配所有数字 const hasSpace = match(/\s+/g); // 返回已经接受了 reg参数 的函数 // 匹配所有空白字符 const hasNumber = match(/\d+/g); // 返回已经接受了 reg参数 的函数 console.log(hasSpace("helloword")); // null 返回已经接受了 reg/str参数 的函数 console.log(hasNumber("123213 123")); // ["123213", "123"] ============================================================ // 再扩展:筛选数组中指定条件的元素 const filter = _.curry((func, array) => { return array.filter(func); }); console.log(filter(hasSpace, ["Tea Meow", "Tea_Meow"])); // ["Patrick Jun"]
5.3 模拟 Lodash.curry()
- 小知识点:func = (a, b, c, d, e) => {},那么 func.length = 5;
function curry(func) { return function curriedFn(...args) { // 判断形参和实参的个数 if (args.length < func.length) { return function () { // 将...args与...arguments拼接传递给curriedFn return curriedFn(...args, ...arguments); }; } return func(...args); }; } function getSum(a, b, c) { return a + b + c; } const curried = curry(getSum); console.log(curried(2, 3)(4)); // 9 console.log(curried(2)(3, 4)); // 9
六. 函数组合
6.1 何谓函数组合?
- 函数组合(compose):如果一个函数 要经过多个函数处理,才能得到最终值,就可以把 中间过程的函数 合并成一个函数
- 解释:函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是 —— 从右到左执行
- 函数组合后只接受一个参数
function reverse(array) { return array.reverse(); } function first(array) { return array[0]; } // 将任意两个函数组合 - 模拟组合函数 从右向左 执行 function compose(f, g) { return function (value) { return f(g(value)); }; } // 将最上面的两个函数组合起来 const last = compose(first, reverse); console.log(last([1, 2, 3, 4])); // 4
6.2 Lodash 组合函数
- Lodash 中的组合函数:flow() / flowRight(),他们都可以组合多个函数,可以参考 6.1 中的解释
- flow():从左到右运行
- flowRight():从右到左运行,使用的更多
const _ = require("lodash"); const reverse = (arr) => arr.reverse(); const first = (arr) => arr[0]; const toUpper = (s) => s.toUpperCase(); const f = _.flowRight(toUpper, first, reverse); console.log(f(["one", "two", "three"])); // THREE
6.3 模拟 Lodash.flowRight()
- 小知识点:数组中的 reduce() 方法 —— 对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值
function compose(...args) { return function (val) { return args.reverse().reduce((acc, fn) => { return fn(acc); }, val); }; } // ES6 // const compose = (...args) => (val) => args.reverse().reduce((acc, fn) => fn(acc), val);
6.4 函数组合中的结合律
- 函数的组合,需要满足结合律
- 举个栗子:compose(f,g,h),先把 f、g 组合在一起,或者先把 g、h 组合在一起;他们再和 f 结合在一起,上述两种方法的返回结果应该一样
console.log(compose(compose(f, g), h) == compose(f, compose(g, h))); //true console.log(compose(f, g, h) == compose(f, compose(g, h))); //true const _ = require("lodash"); // 下面三种写法结果运行一样 const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse); // 前两个组合 const f1 = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse)); // 后两个组合 const f2 = _.flowRight(_.toUpper, _.first, _.reverse); // 不组合 console.log(f(["one", "two", "three"]) === f1(["one", "two", "three"])); // true console.log(f(["one", "two", "three"]) === f2(["one", "two", "three"])); // true console.log(f1(["one", "two", "three"]) === f2(["one", "two", "three"])); // true
6.5 函数组合的 Demo
- 将 NEVER SAY DIE 转换为 never-say-die
- 思路:小写 toLowerCase,分割 split,组合 join
- 'NEVER SAY DIE'.toLowerCase().split(' ').join('-');
const _ = require("lodash"); // 第一步:_.toLower() // 第二步:_.split() // 因为我们需要传入 str 变量,所以 str 放在最后面传入,以下同理 const split = _.curry((symbol, str) => _.split(str, symbol)); // 第三步:._join const join = _.curry((symbol, array) => _.join(array, symbol)); // log 用来检测数据管道中,哪部分值有错误 const log = (v) => { console.log(v); // 继续返回值给下一个fn return v; }; const f = _.flowRight(join("-"), log, split(" "), log, _.toLower); console.log(f("NEVER SAY DIE")); // never-say-die ======================================================================== // // 考虑到数据管道很长的情况,如果多次log,打印的数据不够直观,于是改造log // const _ = require('lodash'); // const trace = _.curry((tag, v) => { // console.log(tag, v); // return v; // }); // const split = _.curry((symbol, str) => _.split(str, symbol)); // const join = _.curry((symbol, arr) => _.join(arr, symbol)); // const f = _.flowRight(join('-'), trace('after split:'), split(' '), trace('after toLower:'), _.toLower); // console.log(f('NEVER SAY DIE'));
七. 函数式编程的思考总结
- 函数式编程是一种强调 以函数使用为主 的软件开发风格;
- 纯函数指:没有副作用的函数,相同的输入有相同的输出;
- 在函数式编程里面,将多个不同函数组合是非常非常非常重要的思想;
- 函数式编程将函数视为积木,通过一些高阶函数来提高代码的模块化和可重用性;
- 柯里化是”因式分解“,将参数分解开;函数组合是”结合律“,将函数组合使用
- 扩展:lodash/fp、函子...
- 参考文章:函数式编程范式 - 思路大前端团队 (ths.js.org)




464

被折叠的 条评论
为什么被折叠?



