【入门】Javascript 函数式编程指南摘抄

135 阅读29分钟

本文内容来自 - 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. 闭包代码例子:

闭包可以访问的内容:

  1. 函数内的所有参数,如number
  2. 外部作用域的所有变量,父级+全局+ 函数声明值后的声明,如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. 函数式编程技巧:柯里化 (重要)

📚在所有参数被提供前,挂起或者延迟函数执行,将多参数函数转换一元函数序列(主要动机)的技术

image.png

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

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结构来延迟异常的抛出

总结:

  1. 面向对象抛异常的机制让函数变得不纯
  2. 把值包裹到容器的模式是为了构建无副作用的代码,把可能不纯的变化包裹成引用透明的过程
  3. 使用Functor将函数应用到容器中的值,是无副作用的,不可变的访问和修改操作
  4. monad是函数式中用来降低应用复杂度的设计模式,通过这种模式可以将函数编排成安全的数据流程
  5. 交错的组合函数和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执行:创建新的活跃上下文,并引用全局上下文中的所有变量,全局上下文执行暂停

image.png

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. 递归调用优化:(重要)

方案一:记忆化优化方案

如果每个子任务都能缓存,就可以减少重复同样的计算,从而提高性能

image.png

方案二:编译器级别的优化-尾递归调用优化

当使用尾调用时,编译器会帮你做尾部调用优化(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 解决方案:

  1. 方式一:持续传递式样,在这种情况下,回调函数被称为当前的延续, 虽然打破了时间依赖,但是开发者会对执行的顺序感到迷惑,所以👇🏻
  2. 方式二:使用一等公民 promise then 链接
  3. 方式三:使用生成器
  4. 方式四:现在更建议使用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:是真实的值

image.png

生成器函数的调用:

  1. 与典型的函调调用不同,生成函数的执行上下文可以暂时暂停,然后随意恢复
  1. 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.响应式范式的好处:

  • 提高代码的抽象级别
  • 充分利用函数式编程中函数链和组合的优势


注解

函数式编程语言有哪些?

  1. Haskell:Haskell 是一种标准的纯函数式编程语言,具有静态类型系统和惰性求值。它支持高阶函数、递归和模式匹配等特性
  2. Lisp:Lisp 可以追溯到上世纪 50 年代早期,是一种基于表达式求值的编程语言。它支持列表操作、递归和闭包等特性,并被广泛应用于人工智能领域。
  3. Scala:Scala 也是一种多范式编程语言,可以与 Java 混合使用。它提供了诸如高阶函数、模式匹配、样例类、类型推断和惰性求值等特性。
  4. Clojure:Clojure 是一个运行在 Java 虚拟机上的方言,具有强大的并发编程能力和内置的不可变数据结构。它也支持多范式编程(面向对象、过程式和函数式)
  5. F#:F# 是运行在 .NET 平台上的跨平台函数式编程语言,可以与 C# 和其他 .NET 语言混合使用。它具有模块化组件开发、异步编程和类型推断等特性
  6. Scheme: 是Lisp的两种主要方言之一(另一种为Common Lisp)

Pointfree风格:

Pointfree这个概念,在编程中叫做隐式编程,同时也叫做无点编程(point-free),其概念是函数定义不用声明参数了的一种编程范式。

pointfree 则是将函数与函数之间以无点的方式组合起来的思想。这里的复合方式(常见的组合子)比如:

  • compose
  • sequence
  • pipe
  • identity
  • ...等方式。

组合子是纯函数,更利于pointfree

pointfree 对错误处理和调试比较困难, 利用函数组合子可以组织程序控制流

函数式Javascript库

  • ladash.js
    • 可实现函数链,函数绑定
  • 函数式库:ramda.js
    • 可轻易实现 参数柯里化,惰性应用,函数组合
    • 所以函数已经被正确柯里化
    • 在组合管道时更具有通用性

为什么要去掉循环的代码?

循环代码是一种重要的命令控制结构

  • 很难重用
  • 很难插入其他操作
  • 为响应新的迭代,代码会不断变化

函数式编程和面向对象编程的关键差异

面向对象的架构里也可以使用函数式编程

区别函数式面向对象
组合单元函数对象(类)
编程风格声明式命令式
数据和行为独立 && 松耦合的纯函数与方法紧耦合的类
状态管理将对象视为不可变的值主张通过实例方法改变对象
程序流控制函数与递归循环与条件
线程安全可并发编程难以实现
封装性因一切不可变,没必要需要保护数据的完整性
着重点在于操作如何执行数据和数据之间的关系
预测性等式正确结果不可预测,可能不一致
  • 面向对象通过特定的行为将很多数据类型逻辑连接在一起,
  • 函数式则关注如何在这些数据类型之上通过组合来连接各种操作。
  • 面向对象的关键是创建继承层次结构,并将方法与数据绑定在一起。
  • 函数式则更倾向于通过广义的多态函数交叉应用于不同的数据类型,同时避免使用this。

有效构建应用程序的方式是混合两种范式。

Interview:

  1. 什么是函数式编程,为什么需要?
  2. 元组和元数是什么关系?
  3. js 高阶函数都有什么?
  4. 什么是一等函数,什么是高阶函数?
  5. 减少元数的方法?
    • 柯里化
    • 部分求值
    • 元组
  6. 在函数式编程中,结合函数有2种方式
    • 组合