本文内容来自 - Javascript 函数式编程指南 [美] 路易斯·阿泰西奥 著
本书主要介绍如何通过ES6将函数式编程技术 应用于代码 来降低代码的复杂性。
ES6 增加了很多特性,能更好的配合函数式编程,如:
- 箭头函数
- const、let
- 迭代器
- 生成器
- promise
- 扩展运算符
- 尾调用优化
第一部分:函数式思想+概念
前言:
选择一个合适的编程模型对编程来说很重要。选择哪种方式,还是混合一起使用,取决你自己。
- 面向对象编程 (OOP),并不是唯一的软件系统规划与架构设计的编程范式。
- 函数式编程 (FP) 已经存在多年,如典型的函数式编程语言Haskell,Lisp, Scala。
定位:
函数式编程不是一种工具或框架,而是一种开发思维。
为什么学习函数式编程?
学习背景:
开发人员的期待:简洁应用结构 + 增加软件扩展性的框架
Web平台的快速演进,和浏览器不断进化的主要原因是用户的需求,人们期望web应用更像是本地桌面,有丰富的响应式的部件,所以JS开发人员就会广泛思考各种解决方案,并适时地采用那些可能提供最优解决方案的编程方式和最佳实践。
开发人员可能遇到的代码问题有:
- 大量使用严重依赖外部共享变量
- 存在太多分支
- 没有清晰的结构大函数
- 共享的可变的全局数据网,难以追踪和调试
学习新的编程思维可以:
- 拓宽视野
- 新的语言会对解决软件问题,提出不同观点
- 降低代码复杂性,增强开发技能
好的设计应该遵循应用设计原则:
- 可扩展
- 可重用
- 可测性
- 易模块化
- 易推理性
函数式编程的好处是什么?
- 通过响应式范式降低【事件驱动代码】的复杂度
- 使用流式的调用链来处理数据
- 促使将任务分解成简单的函数
- 提高代码质量,使代码更高效
函数式编程的核心:
将复杂任务拆分为独立的,粒度较小的组件是构建函数式代码的核心
第一章:什么是函数式编程?
- 定义: 以函数为主的软件开发风格
- 目标:使用函数来抽象作用在数据之上的控制流和操作,从而消除副作用并减少对状态的改变
- 旨在:尽可能的提高代码的 无状态性和不变性
基本概念:
1. 编程范式:
- 声明式编程范式
- 命令式编程范式(更主流,最常见)
- 过程式编程范式(更主流)
📚 函数式编程属于声明式编程范式,易推理,可读性高👇🏻
1.1 声明式编程范式:
- 定义:这种范式会描述一系列的操作,但并不会暴露他们是如何实现的,或者是数据流是如何穿过他们的。
- 关注:
将程序的描述和求值分离开,关注如何用表达式来描述程序逻辑,不一定要指明其控制流和状态的变化。 参考SQL语句。 - 例如:map函数
1.2 命令式编程范式(更主流,最常见):
- 定义:将计算机程序视为一系列的【自上而下的断言】,通过修改系统中各个状态来计算最终的结果
- 关注:告诉计算机
如何执行任务 - 例如: for循环
2. 副作用:读取或改变外部资源
一般来说,函数在读取或者写入外部资源时都会产生副作用,如:👇🏻
- 改变全局变量
- 改变函数参数的原始值
- 处理用户输入
- 抛出一个异常,除非他又被当前函数捕获
- 屏幕打印或记录日志
- 查看html文档,浏览器的cookie, 访问数据库
3. 纯函数:没有副作用和状态变化的函数
不纯的函数结果不可预见,纯函数具有以下性质:👇🏻
- 仅取决于提供的输入,而不依赖于任何【在函数求值期间】或【调用间隔时】可能变化的隐藏状态和外部状态
- 不会造成【超出作用域】的变化,如修改全局变量或引用对象等
- 好处:
- 提高可测试性
- 提高可维护性
3.1 例子:
var counter = 0;
function increment(){
return ++counter;
}
解释:这个函数是不纯的,因为他读取了和修改了一个外部变量,即在函数作用域之外的counter,所以产生了副作用,其结果不可预见,因为conter可以在调用间隔的任何时间被任何人改变。
事实上在一个充满动态与变化的世界里,纯函数是很难使用的, 因为如果一个程序不能修改对象,不能打印到控制台,那他能有什么使用价值呢?但是纯函数并不限制一切状态的变化,他只是提供了一个框架来帮助管理和减少可变状态, 同时让你从不纯里分离纯函数。将函数看做永不会修改的闭合功能单元,可以减少bug的存在。
📢:纯函数的组合是函数式编程的支柱,这些组合技术均利用了函数式抽象的特性,通过柯里化或者部分应用对各种纯函数进行组合
3.2 引用透明(纯函数的特质): 相同的输入有相同的输出
引用透明又称”等式正确性“,最好确保一个函数有相同的返回值。👇🏻
他可以使得函数的结果的一致性和可预测性。因此, 如果一个函数对于相同的输入始终产生相同的结果,那么就说他是引用透明的。
优点:
- 更容易推理整个程序
4. 不可变性:
创建后不能更改的数据
5. 值对象
其相等性不依赖于标识或引用,而只基于值,一旦声明,其状态可能不会被改变
- 基本类型:使用ES6的const
- 复杂类型:使用Object.freeze浅冻结
第二章:高阶JavaScript:
📚JavaScript 也是一门集面向对象和函数式一体的混合语言。
JavaScript 是一种拥有很多共享状态的动态语言,任何时间状态都可以被修改,删除,代替,代码的积累会使得复杂性变高,笨拙而难以维护。
📚动态类型语言:是指在运行期间才去做数据类型检查的语言
1. Lenses函数式引用: 工具库Ramada.js已经实现了
是函数式程序设计中用于访问和
不可改变地操纵数据状态类型属性的解决方案
本质上讲,Lensex与写时复制的策略类似,它采用一个合理管理和赋值状态的内部存储组件。
2. 函数:函数式编程的核心
📚函数:是函数式编程的工作单元和中心,是函数式编程的核心,是个求值表达式
- 表达式: 返回一个值的函数,如函数式编程
- 语句:不返回值的函数, 如命令式和过程式编程
因为函数具有一等性和高阶性,所以JS函数具有值的行为
- 所以说函数是一个【基于输入的且尚未求值的】不可变的值!
- 函数式编程中不应该依赖函数的上下文状态,也就是尽量避免使用this,因为会产生副作用。
- 应该以参数的形式提供给函数
函数支柱性特性一:一等函数(重要)
📚 一等函数:JS中,术语是一等的!将函数视为真实的对象,如下列行为👇🏻
- 函数赋值给变量
- 函数赋值给对象的属性
- 构造函数实例化
- 可以当参数
- 函数可以从其他函数返回
const tyep1 = x=>x+1;
const type2 = {
xx:x=>x+1
}
const type3 = new Function('a','b', 'return a*b')
const type4 = xx.sort((a,b)=>b-a)
const type5 = function(n){
return x=>x+1
}
函数支柱性特性二: 高阶函数 (重要)
📚 高阶函数:以函数作为参数的函数
常见的高阶函数有👇🏻:
- map
- reduce,reduceRight
- filter
- sort
高阶函数的优点:
- 可增强重用性,模块化,扩展性,声明性
特殊的高阶函数:闭包(重要)
📚 闭包: 能访问作用域之外的变量 的函数
1. 闭包代码例子:
闭包可以访问的内容:
- 函数内的所有参数,如number
- 外部作用域的所有变量,父级+全局+ 函数声明值后的声明,如amount
// 函数内返回函数,是个闭包,第二个函数内可以访问不是他作用域内的变量amount
function makeAddFun(amount){
return function (number){
return amount + number
}
}
// 调用方式
makeAddFun(3)(2) // 5
2. 闭包存在的问题:有歧义的循环计数器
var els = document.querySelectorAll('button');
for(var i=0; i<els.length ; i++){
els[i].onclick = function(){
alert( '第'+ i +`按钮` )
}
}
❌ 结果都是第三个。因为闭包共享变量,所以闭包里的i是变成了3的i。
解决方案:
- 使用更多的闭包
- 使用ES6的let变量
- 使用立即执行函数
3. 闭包的实际应用:
- 模拟私有变量
- 其他语言有private来保证变量私有化,js没有,所以用闭包模拟
- 管理全局命名空间,以免在全局范围内共享数据
- 早期模块化方案IIFE
- 异步服务端调用
js中的一等高阶函数可以作为【回调函数】传递到其他函数中,异步处理最常用的方式就是提供一个callback来处理服务端的响应
- 创建人工块作用域变量
ES5(JS)缺乏【块作用域】,可以用es6的let解决,也可以使用闭包来解决
第二部分:函数式基础
第三章: 轻数据结构,重操作
1. lambda表达式
匿名函数的简写方式
num => Math.pow(num,2),也就是(JS)es6的箭头函数
2. 一元函数
一元函数是指函数方程式中只包含一个自变量。例如y=F(x)
3. 控制流
程序为实现业务目标所要行进的路径被称为控制流
4. 方法链
一种能够在一个语句中调用多个方法的面向对象编程模式
当这些方法属于同一个对象时候,方法链又称为方法级联
5. 函数链
函数链的建立基于一些指向不同代码片段的函数名,他们将作为整个表达式的各部分被执行
- 接受函数为参数,注入解决特定任务的特定行为
- 代替for循环,减少要维护的错误
链式调用优点:
链式操作能使程序更简洁,流畅并富有表现力,易读
6. 递归:(重要)
通过将问题分解为较小的自相似问题来解决问题本身的技术,包含👇🏻
- 基例(终止条件)
- 递归条件
例子:通过递归深冻结:
var isObject = val=>val&&typeof val ==='object';
function deepFreeze(obj){
if(isObject(obj) && !Object.isFrozen(obj)){
Object.keys(obj).forEach(name=>deepFreeze(obj[name]));
Object.freeze(obj);
}
return obj;
}
尾调用优化
函数体的最后步骤是 递归调用,是ES6添加的编译器增加功能(第7章有优化的详细分析)
第四章:模块化且可重用的代码
1. 函数管道化
📚 管道:以函数作为组件,将一个函数的输出会作为下一个函数的输入。
1.1 方法链 与 函数管道化 区别:
| 标题 | 方法链路 | 管道化 |
|---|---|---|
| 特点 | 紧耦合,有限的表现力 | 松耦合,灵活 |
| 实现 | 需调用所属对象的方法 | 自由排列所有独立的函数操作 |
1.2 实现管道的 要求:
- 类型:函数的返回类型必须与接收函数的参数类型匹配
- 元数:接收函数必须声明 至少一个参数,才能处理上一个函数的返回值
1.2.1 降低元数的方式:
- 元组
- 柯里化
2. 元组:
元数定义为:函数所接收的参数数量,也被称为函数的长度(length)
在函数式编程中,函数参数数量往往与其复杂性成正比,只具有单一参数的纯函数是最简单的。但是实际总使用一元函数并非那么容易,在真实的世界中,如果函数需要返回2个不同的值(正确的结果 和错误信息),函数式语言通过元组的结构来做到这一点!
元组: 有限的,有序的元素列表,是不可变的结构
2.1 当涉及到函数间的数据传输时,元组能够具有更多的优点:👇🏻
- 不可变的
- 避免创建临时类型
- 避免创建异构数组
可以使用一个元组作为函数的返回值,将不同类型的元素打包在一起,以便将他们传递到其他函数中。
JS 原生并不支持元组的数据类型,但是提供了实现元组所需的工具。
元组是减少函数元数的方式之一,同时柯里化也可以降低元数。
3. 函数式编程技巧:柯里化 (重要)
📚在所有参数被提供前,挂起或者延迟函数执行,将多参数函数转换为一元函数序列(主要动机)的技术
3.1 当使用部分参数调用函数时:柯里化会挂起
如 fun(a,b,c) 但只传递了fun(3)
- 非柯里化缺参数: 实参会变成undefined,
fun(3,undefined, undefined) - 柯里化:要求所有参数被明确定义,部分参数时,会返回一个新的函数
3.2 柯里化好处:
- 增加代码模块化
- 增加可重用性
- 降低元数,也就降低了函数的复杂度
- 简化了函数的组合
3.3 手动柯里化:利用闭包
JS语言原生不支持柯里化,在纯函数式语言中,柯里化是原生特性
// 柯里化是一种词法作用域(闭包)
function curry2(fn){
return function(arg1){
return function(arg2){
return fn(arg1,arg2)
}
}
}
// 调用
curry2( (x,y)=>x+y )(2)(3) // 5
3.4 自动柯里化:使用库
使用ramda.js库 R.curry()
3.5 柯里化的实际应用
- 仿真函数接口
- 实现可重用模块化函数模板
- 创建可抽象函数行为的函数包装器:可预设其参数或对其部分求值
3.6 柯里化的替代方案:部分应用和函数绑定
部分应用:通过将函数的不可变参数子集 初始化为固定值 来创建更小元数函数的操作
3.6.1 部分应用和柯里化的关系:
和柯里化一样,部分应用也可以用来缩短函数的长度.
柯里化的函数本质上是部分应用的函数
柯里化是一种部分应用的自动化使用方式
| 标题 | 柯里化 | 部分应用 |
|---|---|---|
| 参数传递 | 嵌套的一元函数,允许传递部分参数 | 将函数参数与一些预设值绑定(赋值) |
| 控制 | 函数求值的时间与方式 | 闭包内包含已赋值参数,在之后的调用中被完全求值 |
3.6.2 部分应用的实际用途:
- 核心语言扩展
- 在延迟函数setTimeout中,部分应用会失效
- 解决方案: 延迟函数绑定 bind
- 在延迟函数setTimeout中,部分应用会失效
4. 组合函数管道
纯且无副作用的函数 使得组合成为一种非常强大的技术
无副作用的函数一切必须以参数的形式提供,为了能够正确的使用组合,所选函数必须是无副作用的
4.1 组合
- 组合是编排整个js解决方案的粘合剂。
- 通过组合一些小的高阶函数来创建有意义的表达式,可以简化很多繁琐的程序。
- 组合的概念不仅限于函数,整个应用程序都可以通过 无副作用的纯的程序或者模块组合而成
- 组合是一种合取操作,这意味着 其元素使用 逻辑与运算链接
4.2 描述与求值分离
函数式组合的强大之处:将函数的描述与求值分开
// fn1, fn2 在组合中是静止的
R.compose(fn1, fn2);
底层原理:
两个函数的组合是:将第二个函数的输入 直接映射到 第一个函数 输出的新函数
组合后的函数也是输入和输出之间的【引用透明映射】
compose 的实现:
JS 没有提供内置的Compose 函数(也是一个高阶函数),但是可以自己实现,而且Ramda库提供了。
function compose(fns){
let args = arguments;
let start = args.length -1;
return function(){
let i = start;
let result = args[start].apply(this,arguments);
while(i--){
result = args[i].call(this,result)
}
return result;
}
}
第五章 针对复杂应用的设计模式
异常(错误)处理:空值null检查
异常应该由一个地方抛出,而不应该随处可见
- 命令式编程: try catch 调用堆栈跟踪
- 函数式编程: 不适合使用try catch方式
- 解决思路:创建一个安全的容器,来存放危险的代码
- 解决方案:如下👇🏻
方案一、 Functor:包裹
functor本身并不需要知道如何处理null
只是一个将函数应用到它包裹的值上,并将结果再包裹起来的数据结构,可链接
很多人一直使用Functor而没有意识,但其实我们使用过,比如 :
- map: 由于引用透明性,只有输入一样,输出就一样
- filter
- compose
1.1 Functor 重要的属性约束:
- 必须是无副作用的
- 必须是组合的
1.2 为什么要有约束:
遵循规则可以免于抛出异常,篡改元素或者改变函数的行为。
其实际目的只是创建一个上下文或者抽象,以便更安全的应用操作到值,而不改变原始值。
1.3 Functor的局限性:
如果两个函数都被包裹一个安全的上下文,2个函数组合后,返回的值会被包裹2层
方案二、 Monad: 简化代码中的错误处理
用于创建一个带有一定规则的容器,而Functor并不需要了解其容器内的值
Functor 可以有效的保护数据,然而当需要组合函数时,即可以用Monad来安全并且没有副作用的管理数据流, 总之Monad 是用来消除Functor的不足。
2.1 Monad 实例
- Maybe:擅长集中管理的无效数据的检查,但没有表明失败的原因
- Either:代表2个逻辑分离的值,a和b 永远不会同时出现, 通常操作右值
- Left:包含一个可能的错误消息或者抛出的异常对象
- Right:包含一个成功的值
- IO: 与外界交互
- 明显将不纯分离出来
- 使用Monadic容器作为返回类型的好处
- 保持了函数的一致和类型的安全
- 也保留了引用透明性
❓为什么不使用前面说的元组来捕获:因为元组代表的是逻辑关系是与,错误处理是互斥类型,因为不会同时出现成功和错误两种情况
2.1.1 函数式编程通常使用Maybe 和Either 用来:
- 隔离不纯
- 合并判空逻辑
- 避免异常
- 支持函数组合
- 中心化逻辑,用于提供默认值
函数式编程可以避免动不动就抛出异常,可以通过将异常对象存储到left结构来延迟异常的抛出
总结:
- 面向对象抛异常的机制让函数变得不纯
- 把值包裹到容器的模式是为了构建无副作用的代码,把可能不纯的变化包裹成引用透明的过程
- 使用Functor将函数应用到容器中的值,是无副作用的,不可变的访问和修改操作
- monad是函数式中用来降低应用复杂度的设计模式,通过这种模式可以将函数编排成安全的数据流程
- 交错的组合函数和monadic类型是非常有弹性和强大的,如maybe,either,io
第三部分:函数式技术提升
第六章:单元测试(省略...,不介绍)
第七章:函数式优化
1. 优化:
如果用柯里化,递归,monad 和组合来解决最简单的问题,能保证性能吗?现在的web计算器已经非常快了, 并且编译器技术也很先进了,这保证了函数式编程的性能。
函数式不会加快单个函数的求值速度,理念:是避免重复的函数调用 + 延迟调用。
优化不需要过早
1.1 函数式JavaScript性能优化
一些函数式编程语言本身内置了优化,但是js需要自定义代码或者函数库来进行这些优化
2. 堆栈:
2.1 栈:
是LIFO后进先出的基本数据结构,所以操作从顶部开始
2.2 堆栈的行为由下列规则确定:
- js是单线程
- 有且只有一个全局上下文
- 函数上下文数量有限制(浏览器不同,限制不同)
- 每个函数调用会创建一个新的执行上下文,递归调用也如此
2.3 堆栈可能增长(溢出)的原因:
每次迭代循环后都能被回收
- 柯里化
- 不正确的递归
2.4 上下文堆栈:(重要)
由于函数式编程依赖函数求值,所以有必要知道函数调用时发生了什么,也必须了解函数调用的推移
JS中,函数调用会在【函数上下文堆栈中】创建记录(帧)
全局上下文先入栈,所以始终在堆栈的底部。
- 每个函数的上下文帧都占用一定的内存,实际大小取决她的局部变量的个数:
- 空帧:48字节
- 数字,boolean: 8字节
JS中上下文堆栈负责:
- 管理函数执行
- 关闭变量作用域
作用域链: 内部函数能访问到外部函数的闭包
所以函数的作用域链最终都链接到全局上下文
2.5. 柯里化与函数上下文堆栈 :
嵌套的函数会使用更多的堆栈,柯里化和递归都依赖嵌套函数,堆栈的增长和嵌套成正比
1.非柯里化调用:
logger执行:创建新的活跃上下文,并引用全局上下文中的所有变量,全局上下文执行暂停
2.柯里化调用存在的问题:
因为嵌套调用,会占用大量内存,过度使用会占用大量的堆栈空间,影响运行性能。
比如 logger函数调用了log4函数: log4 会在其上占用分配给logger的存储器。log4执行时,logger被暂停,等log4执行完成,弹出log4 和logger 的堆栈
3. 优化手段一:惰性求值(可以延迟执行)
避免大量函数推入堆栈的一个解决方案:避免不必要的调用
函数链:就是惰性计算, 就是需要的时候才执行,有利于性能。
📚求值方式:
- 惰性求值:Haskell语言
- 可以根据程序的要求生成任意大小的列表
- 及早求值(贪婪求值):JavaScript
3.1 如何利用惰性求值(技巧):
- 避免不必要的计算
- 模拟惰性传值:传递函数名称,自行决定什么时候调用
- 使用组合子:因为组合子使用的是函数的引用
- 记忆化
- 使用函数类库
- shortcut fusion技术:lodash 有带有 shortcut fusion的函数
3.1.1 记忆化
缓存方案可能会有副作用,因为用到了全局变量,所以有了记忆化方案,但方案类似。
纯函数语言自带记忆化,JavaScript 没有自动记忆化的支持,但是可以给原型添加
记忆化把函数当做一个惰性计算的返回值
1. 柯里化与记忆化:
复杂的函数产生缓存键逻辑会复杂,但是又不应该给函数增加额外的开销和复杂度,所以柯里化就可以解决这个问题:
- 柯里化可以在组合前选择将部分函数进行记忆化
2. 分解: 实现更大程序的记忆化
问题拆分的越小,越容易记忆化
同样递归也可以分解
4. 递归调用优化:(重要)
方案一:记忆化优化方案
如果每个子任务都能缓存,就可以减少重复同样的计算,从而提高性能
方案二:编译器级别的优化-尾递归调用优化
当使用尾调用时,编译器会帮你做尾部调用优化(TCO).
也称尾部调用消除,是ES6天假的编译器增强功能
能被优化的原因:
函数的最后一件事如果是递归的函数调用,那么运行时会认为不必要保持当前的栈帧,因为所有工作已经完成,可以抛弃了。 通过这种方式,递归每次都会创建一个新的帧,回收旧的帧,而不是堆叠。
递归调用会在栈中不断堆叠,当算法满足终止条件时候,运行时就会展开调用栈,并执行对应操作,因此所有返回语句都将被执行。递归就是通过语言运行时这种机制代替了循环。
例子:阶乘函数的尾递归优化步骤:
这个不是尾调用:
const factorial = (c)=>(n===1) ? 1 : (n * factorial(n-1));
- 将当前乘法结果当前参数传入递归函数
- 使用ES6的默认参数给一个默认值
// 优化后
const factorial = (c, current=1)=>(n===1) ? current : factorial(n-1, n*current)
尾递归带来递归循环的性能接近于for循环,尾调用也可以调用其他函数,不只是递归。
第八章:异步管理事件以及数据
本章目的:将函数式编程和es6的promise结合使用,并引入响应式编程,这两种方式可以将地狱调用转换成优雅流畅的表达式。
1. 函数式编程解决异步数据流:
在实际操作中,程序不全是同步的,需要通过ajax与远程服务器通信等异步交互,并将返回的数据显示在web页面上,所以整个流程不是线性的,那么就会存在以下问题:👇🏻
- 在函数之间有时间依赖关系
- 不可避免的陷入回调金字塔
- 同步和异步代码的不兼容
1.1 问题:回调地狱原因
函数需要嵌套的原因是因为内部函数的计算依赖其外部变量
1.2 解决方案:
- 方式一:持续传递式样,在这种情况下,回调函数被称为当前的延续, 虽然打破了时间依赖,但是开发者会对执行的顺序感到迷惑,所以👇🏻
- 方式二:使用一等公民 promise then 链接
- 方式三:使用生成器
- 方式四:现在更建议使用await等待
2. 生成器(Generator)
生成器可以调用生成器(递归调用)
生成器的最大的优点之一是:可以惰性求值
2.1 惰性数据:
ES6 最强大的功能之一:就是可以通过暂停函数执行而不用一次执行完
函数可以生成惰性数据:而不必一次处理大量数据
如果js也是惰性求值,理论上可以做R.range(1, Infinity).take(3)。 但是JS 属于及早求值,所以对这个调用将无法完成,并会溢出浏览器的函数栈。所以需要生成器函数。
2.2 生成器函数:
生成器函数:通过语言级别的
function*符号定义,返回一个生成器对象,可以使用关键字yield暂停执行生成器函数
- 生成器函数定义:
// 生成器函数
function *range(start=0,finish=3){
for(let i= start; i < finish;i++){
yield i;
}
}
- 生成函数返回:符合迭代器协议的Generator对象
- 生成器对象包含一个next方法,我们将用next方法来迭代生成器对象
next():返回一个对象,{done:false, value:xx}- done为false:表示生成器还可以继续迭代
- value:是真实的值
- 生成器对象包含一个next方法,我们将用next方法来迭代生成器对象
生成器函数的调用:
- 与典型的函调调用不同,生成函数的执行上下文可以暂时暂停,然后随意恢复
- ES6的
for...of可以用于生成器的循环, ES6的扩展运算符可以展开生成器对象
// 方式一:
const num = range(1)
num.next().value; // 走到yield并暂停,此时是1
num.next().value; // 2
// 方式二:可以使用for...of
for(let n of range(1)){
console.log(n)
}
2.3 生成器作用:
- 惰性求值:可以惰性的从无限集中取一定数量的元素
- 生成器函数:可以递归的使用
- 可以用作观察器函数
- 抽象异步代码
2.4 迭代器(iterator)协议:
可以使用
xx[Symbol.iterator]判断返回为undefined则没有实现迭代器协议,比如对象就没实现
2.4.1 谁实现了迭代器协议:
- 字符串
- 数组
- 生成器
2.5 生成器和迭代器的关系:
为什么可以使用for of 循环生成器?
因为生成器实现了迭代器协议。生成器被调用时候会在内部产生一个迭代器iterator,以此提供惰性行为。迭代器会在每次被调用时候通过yield返回相应数据。生成器和迭代器具有相似的语义
3. 响应式编程:基于函数式编程
1. Observable 订阅:
observable是可以订阅的数据对象, 订阅数据流,开发者通过组合和链式操作来优雅的处理数据
流:是随着时间发生的有序事件的序列,要取值,先订阅,流带来的是声明式的代码和链式计算
2. 函数式响应式编程(FRP):
响应式编程和函数式编程没什么区别,只是使用不同的工具集而已,响应式编程更倾向于和函数式编程一起使用,从而产生函数式响应式编程(FRP)
3.响应式范式的好处:
- 提高代码的抽象级别
- 充分利用函数式编程中函数链和组合的优势
注解
函数式编程语言有哪些?
- Haskell:Haskell 是一种标准的纯函数式编程语言,具有静态类型系统和惰性求值。它支持高阶函数、递归和模式匹配等特性。
- Lisp:Lisp 可以追溯到上世纪 50 年代早期,是一种基于表达式求值的编程语言。它支持列表操作、递归和闭包等特性,并被广泛应用于人工智能领域。
- Scala:Scala 也是一种多范式编程语言,可以与 Java 混合使用。它提供了诸如高阶函数、模式匹配、样例类、类型推断和惰性求值等特性。
- Clojure:Clojure 是一个运行在 Java 虚拟机上的方言,具有强大的并发编程能力和内置的不可变数据结构。它也支持多范式编程(面向对象、过程式和函数式)。
- F#:F# 是运行在 .NET 平台上的跨平台函数式编程语言,可以与 C# 和其他 .NET 语言混合使用。它具有模块化组件开发、异步编程和类型推断等特性
- Scheme: 是Lisp的两种主要方言之一(另一种为Common Lisp)
Pointfree风格:
Pointfree这个概念,在编程中叫做隐式编程,同时也叫做无点编程(point-free),其概念是函数定义不用声明参数了的一种编程范式。
pointfree 则是将函数与函数之间以无点的方式组合起来的思想。这里的复合方式(常见的组合子)比如:
- compose
- sequence
- pipe
- identity
- ...等方式。
组合子是纯函数,更利于pointfree
pointfree 对错误处理和调试比较困难, 利用函数组合子可以组织程序控制流
函数式Javascript库
- ladash.js
- 可实现函数链,函数绑定
- 函数式库:ramda.js
- 可轻易实现 参数柯里化,惰性应用,函数组合
- 所以函数已经被正确柯里化
- 在组合管道时更具有通用性
为什么要去掉循环的代码?
循环代码是一种重要的命令控制结构
- 很难重用
- 很难插入其他操作
- 为响应新的迭代,代码会不断变化
函数式编程和面向对象编程的关键差异
面向对象的架构里也可以使用函数式编程
| 区别 | 函数式 | 面向对象 |
|---|---|---|
| 组合单元 | 函数 | 对象(类) |
| 编程风格 | 声明式 | 命令式 |
| 数据和行为 | 独立 && 松耦合的纯函数 | 与方法紧耦合的类 |
| 状态管理 | 将对象视为不可变的值 | 主张通过实例方法改变对象 |
| 程序流控制 | 函数与递归 | 循环与条件 |
| 线程安全 | 可并发编程 | 难以实现 |
| 封装性 | 因一切不可变,没必要 | 需要保护数据的完整性 |
| 着重点 | 在于操作如何执行 | 数据和数据之间的关系 |
| 预测性 | 等式正确 | 结果不可预测,可能不一致 |
- 面向对象通过特定的行为将很多数据类型逻辑连接在一起,
- 函数式则关注如何在这些数据类型之上通过组合来连接各种操作。
- 面向对象的关键是创建继承层次结构,并将方法与数据绑定在一起。
- 函数式则更倾向于通过广义的多态函数交叉应用于不同的数据类型,同时避免使用this。
有效构建应用程序的方式是混合两种范式。
Interview:
- 什么是函数式编程,为什么需要?
- 元组和元数是什么关系?
- js 高阶函数都有什么?
- 什么是一等函数,什么是高阶函数?
- 减少元数的方法?
- 柯里化
- 部分求值
- 元组
- 在函数式编程中,结合函数有2种方式
- 链
- 组合