写在前面
一直计划准备一些博客,种种原因搁浅(主要还是没时间。。。)。最近公司里的项目逐渐稳定下来了,前端的开发节奏虽然很快,但是都能实现,剩下的开发大多其实是繁复的代码组装的体力活。哈哈哈...遂给自己开了这个专栏,一是为了督促自己记录下编写过程和一些踩的坑,跟大家分享;二是写点自己想写的代码,让自己开心一些吧!
总纲
基础
ECMASCRIPT
JAVASCRIPT
进阶
FLOW.js——js的类型检测工具
TYPESCRIPT——js超集
Functor——函数式编程

FLOW.js——js类型检测工具
1.简介
JS作为一种脚本语言是没有类型检测的,这个特点有时候用着很爽,但当你在一个较大的项目中的时候,就会发现这其实是一件挺糟糕的事情,因为和你协作的程序员往往不太清楚你所写的代码到底哪种类型才是正确的,而且代码重构的时候也很麻烦。于是基于这个需求有了Typescript和Flow的产生
Flow.js是FaceBook发布的开源Javascript静态类型检查器。他给JavaScript提供了静态类型来提高开发人员的生产力和代码质量。
2.优点
类型检测
只能提示更加完善,因为js很多时候编辑器不知道你的类型,就无法提供正常的提示
快速重构
类型注解,方便开发人员理解项目代码
较之typescript使用学习成本更低,因为flow只是一个辅助工具
3.安装
初始化项目 yarn init -y
安装flow 模块 yarn add flow-bin --dev
初始化flow yarn flow init
启动flow yarn flow
4.使用
在需要类型检测的文件第一行加入
4.1 类型推断
即使没有编写类型注释的情况下flow依旧可以根据我们进行的一些运算智能的推断,不过不建议这么做,类型看起来不够明确。

4.2 类型标注
4.2.1 原始类型标注
const a: string = 'zhihu'; const b: number = 5; const c: boolean = false; const d: void = undefined; const e: null = null;以上需要注意的几点:
类型标注都是小写的
null的类型是null
undefined的类型是void
4.2.2函数标注
函数标注表示返回结果的数据类型
// 函数声明 function foo () :number { return '100' //string [1] is incompatible with number [2]}function bar () :void{}// 函数表达式const foo=function() :string{ return 100 //number [1] is incompatible with string [2]}// 箭头函数foo=() :null=>{ return 'a' //string [1] is incompatible with null [2]}4.3 数组类型
- Array 后接一对尖括号,且尖括号里边就是数组项的类型名称;,对于jsx不友好,会被错误解析
const arr1:Array<number>=[12,23,3]
类型名称后加一对中括号。
const arr2:number[]=[1,23,2]
4.4 对象类型
变量名后跟对象形式的类型注解即可对对象进行类型注解,个数和下标需要一一对应
const obj:{foo:string,bar:number}={foo:'sss',bar:12}在初始定义时不确定属性是否需要定义的时候可以在key后加?表示可选。
const obj2:{foo?:string,bar:number}={bar:1} //?表示可选更优雅的方式是用type 去写,会很清晰
type obj3Type={ foo:string, bar:number}//type 是 Flow 中的关键字,用来定义自定义的类型const obj3:obj3Type={foo:'312',bar :12}初始定义未定义但后续添加又需要类型检测时,可以用类似索引器的写法
//需要动态添加的可以,用这种类似索引器的语法const obj4: { [string]: string } = {}obj4.key=1312
4.5 字面量类型注解
const a:'foo'='foo'//通过const a:'111'='foo'//Cannot assign `'foo'` to `a` because string [1] is incompatible with string literal `111` 4.6 或类型注释
type bType=string|1232|[]const b :bType ='foo'4.7 MAYBE 类型
// 相当于// const gender: number | null | void = undefinedconst gender:?string=undefined;注意:
4.5 字面量类型必须严格相等
4.6或不是||而是|
4.8 MIXED混合类型
可以理解为6大数据类型都可以,但是在这还是强类型的,第一次明确变量类型的时候就被检测到了,后续再检测到其他类型就会报错。
// string | number | boolean | ....function passMixed(value: mixed) { // value.substr(1) // value * value //报错 if (typeof value === 'string') { value.substr(1) } if (typeof value === 'number') { value * value }}passMixed('string')passMixed(100)4.9 ANY任意数据类型
任意数据类型都可以,且是动态的,主要是用来兼容项目里的不知道数据类型时候的代码。
function passAny(value: any) { value.substr(1) value * value}passAny('string')passAny(100)5.输出
5.1 flow自带的移除类型检测模块
安装
yarn add flow-remove-types 使用
yarn flow-remove-types . -d dist //将所有js文件输出到dist文件夹下yarn flow-remove-types src -d dist //将所有src目录下的js文件输出到dist文件夹下5.2 使用babel
安装
yarn add @babel/core @babel/cli @babel/perset-flow//碰到安装失败一定要找到错误的模块,然后remove再安装touch .babelrc文件 ,输入
{ "presets": ["@babel/preset-flow"] } 使用
yarn babel src -d dist
推荐插件
flow-language //编译阶段就可以进行检测
TYPESCRIPT——ts
1.简介
TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,TypeScript 在 JavaScript 的基础上添加了可选的静态类型和基于类的面向对象编程。
其实TypeScript就是相当于JavaScript的增强版,但是最后运行时还要编译成JavaScript。TypeScript最大的目的是让程序员更具创造性,提高生产力,它将极大增强JavaScript编写应用的开发和调试环节,让JavaScript能够方便用于编写大型应用和进行多人协作。
2.与js的对比
TypeScript是一个应用程序级的JavaScript开发语言。(这也表示TypeScript比较牛逼,可以开发大型应用,或者说更适合开发大型应用)
TypeScript是JavaScript的超集,可以编译成纯JavaScript。这个和我们CSS离的Less或者Sass是很像的,我们用更好的代码编写方式来进行编写,最后还是有好生成原生的JavaScript语言。
TypeScript跨浏览器、跨操作系统、跨主机、且开源。由于最后他编译成了JavaScript所以只要能运行JS的地方,都可以运行我们写的程序,设置在node.js里。
TypeScript始于JavaScript,终于JavaScript。遵循JavaScript的语法和语义,所以对于我们前端从业者来说,学习前来得心应手,并没有太大的难度。
TypeScript可以重用JavaScript代码,调用流行的JavaScript库。
TypeScript提供了类、模块和接口,更易于构建组件和维护。
typescript可以支持最新的es标准且可以配置输出成es6或者以下的版本。
3.安装
初始化项目
yarn init -y安装typescript
yarn add typescript --dev //碰到卡顿的情况,切换npm初始化ts配置文件
yarn tsc --init
4.使用
4.1 原始数据类型
和flow.js基本一致
const a:string='hello'const b:number=12const c:object={Typescript:'word'}const d:null=null;const e:undefined=undefined;//如果这边出现报错,就说明tsconfig.json中的target为es6以下版本// 因为Symbol是es6中添加的数据类型// 使用它的前提是必须确保有对应的 ES2015 标准库引用// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015const f: symbol = Symbol();const g:boolean=true// 表示没有任何返回值的函数:function alert():void{ console.log(312) // return 123}// 声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null:let unusable: void = undefined;4.2 作用域问题
作用域问题默认文件中的成员会作为全局成员多个文件中有相同成员就会出现冲突const a = 123解决办法1: IIFE 提供独立作用域(function () { const a = 123})()解决办法2: 在当前文件使用 export,也就是把当前文件变成一个模块模块有单独的作用域// const a:number=1 //'a' was also declared here.const a=123export {}4.3 对象类型
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
什么是接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
export {}const foo:object=function(){}const obj:{foo:number}={foo:123}interface Person{//接口 num:number; name:string;}const obj :Person={ num:132, name:'312'}接口生命的数据类型,不仅类型需要匹配上,数量、key也需要匹配上
interface Person{ num:number; name:string; gender:boolean // 'gender' is declared here.}const obj :Person={ num:132, name:'312'}//Property 'gender' is missing in type '{ num: number; name: string; }' but required in type 'Person'.4.4 数组类型
export {} // 确保跟其它示例没有成员冲突// 数组类型的两种表示方式const arr1: Array<number> = [1, 2, 3]const arr2: number[] = [1, 2, 3]//传入的所有参数都必须是numberfunction sum(...args:number[]){ console.log(args)}sum (1,23,2)4.4 元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
元组起源于函数编程语言(如 F#),这些语言中会频繁使用元组。
export { }const tuple: [string, number] = ['312321', 232]const entries: [string, number][] = Object.entries({ foo: 23, bar: 400})const [ke,value ]=entries;4.5 枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
- 数字枚举,枚举值自动基于前一个值自增
// enum PostStatus {// Draft = 6,// Unpublished, // => 7// Published // => 8// } 字符串枚举
4.6 函数类型
export {} // 确保跟其它示例没有成员冲突function func1 (a: number, b: number = 10, ...rest: number[]): string { return 'func1'}func1(100, 200)func1(100)func1(100, 200, 300)// -----------------------------------------const func2: (a: number, b: number) => string = function (a: number, b: number): string { return 'func2'}函数声明法function add(n1: number, n2: number): number { return n1 + n2;};console.log(add(1, 2)); //312345函数表达式法var add = function (n1: number, n2: number): number { return n1 + n2;};console.log(add(1, 2)); //312345箭头函数var add = (n1: number, n2: number): number => { return n1 + n2;}console.log(add(1, 2)); //3————————————————版权声明:本文为CSDN博主「浅殇若梦」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/HeiXueJi/article/details/849916204.7 any
// 任意类型(弱类型)export {} // 确保跟其它示例没有成员冲突function stringify (value: any) { return JSON.stringify(value)}stringify('string')stringify(100)stringify(true)let foo: any = 'string'foo = 100foo.bar()// any 类型是不安全的4.8 断言
<>res断言和res*res
// 类型断言export {} // 确保跟其它示例没有成员冲突// 假定这个 nums 来自一个明确的接口const nums = [110, 120, 119, 112]const res = nums.find(i => i > 0)// const square = res * resconst num1 = res as numberconst num2 = <number>res // JSX 下不能使用4.9 枚举
export { } // 确保跟其它示例没有成员冲突interface Post { title: string content: string subtitle?: string readonly summary: string}const hello: Post = { title: 'Hello TypeScript', content: 'A javascript superset', summary: 'A javascript'}interface Cache { [prop: string]: string}const cache: Cache = {}cache.foo = 'value1'cache.bar = 'value2'4.10 class
export {} // 确保跟其它示例没有成员冲突class Person { name: string age: number constructor(name: string, age: number) { this.age = age; this.name = name } }访问修饰符
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。
public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public的private修饰的属性或方法是私有的,不能在声明它的类的外部访问protected修饰的属性或方法是受保护的,它和private类似,区别是它在子类中也是允许被访问的
export { }class Person { public name: string private age: number protected gender: boolean constructor (name:string,age:number){ this.name = name this.age = age this.gender = true }}console.log(Person.name)console.log(Person.age)console.log(Person.gender)4.11 readonly
只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。
class Animal { readonly name; public constructor(name) { this.name = name; }}let a = new Animal('Jack');console.log(a.name); // Jacka.name = 'Tom';// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。
class Animal { // public readonly name; public constructor(public readonly name) { // this.name = name; }}4.12 类与接口
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。
export{}interface Eat{ eat(food:string):void}interface Run{ run(distance: number) :void}class Person implements Eat, Run { eat(food: string): void { console.log(`优雅的进餐: ${food}`) } run(distance: number) { console.log(`直立行走: ${distance}`) }}class Animal implements Eat, Run { eat(food: string): void { console.log(`呼噜呼噜的吃: ${food}`) } run(distance: number) { console.log(`爬行: ${distance}`) }}4.12 抽象类
abstract 用于定义抽象类和其中的抽象方法。
抽象类是不允许被实例化的:
抽象类中的抽象方法必须被子类实现:
export{}abstract class An{ eat(food:string) :void{ console.log(`呼噜呼噜的吃: ${food}`) } abstract run (distance:number):void}class Dog extends An{ run(distance: number): void { console.log(distance) }}const d=new Dogd.eat('321')d.run(31231)4.13 泛型
与泛型接口类似,泛型也可以用于类的类型定义中:
function createArray(length:number,value:number):number[]{ const array=Array<number>(length).fill(value) return array}function createObjArray(length:number,value:object):object[]{ const array=Array<object>(length).fill(value) return array}//上面两种函数都是实现填充数组但是因为数据类型不同的原因写了,很冗余,// 于是就可以用到泛型了/function create<T> (length:number ,value :T):T[]{ const array=Array<T>(length).fill(value) return array}const res=create(1,23)4.14 类型申明模块
在实际开发中,很多时候我们会去引入npm包,现在很多生态都还没支持ts,也就是类型检测。在引入的时候ts会报错,没有检测到类型申明。这个时候如果ts社区已经有了相应的生态,则错误提示会有建议下载的包,npm 添加使用即可,如果没有的话,就需要自己进行类型申明。
import { camelCase} from 'lodash'declare function camelCase(input:string):string;camelCase('111')5.输出
yarn tsc ./01-getting-start.ts //输出某一ts文件,该情况无法使用tsconfig.json的配置yarn tsc //输出整个目录内的ts文件Functor——函数式编程

1、为什么要学习函数式编程
函数式编程是非常古老的一个概念,早于第一台计算机的诞生。
1.1那我们为什么现在还要学函数式编程?
函数式编程是随着 React 的流行受到越来越多的关注
Vue 3也开始拥抱函数式编程
函数式编程可以抛弃 this
打包过程中可以更好的利用 tree shaking 过滤无用代码
方便测试、方便并行处理
有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda
1.2什么是函数式编程
函数式编程(Functional Programming, FP),FP 是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。
面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系
函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和 输出的函数 x -> f(联系、映射) -> y,y=f(x)
函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x和y的关系
相同的输入始终要得到相同的输出(纯函数)
函数式编程用来描述数据(函数)之间的映射
2、函数的概念
2.1函数是一等公民
函数可以作为变量存储
函数可以作为传参
函数可以作为返回值
综上所述函数也是对象,但它可以做对象不能做的事,例如回调,异步等等。
2.2高阶函数
Higher-order function
接收一个值或者函数返回一个函数或者值。
// foreachconst forEach=(array:number[],fn:any):void=>{ for(let i:number=0 ;i<array.length;i++){ fn(array[i]) }}forEach([1,2,32,3], (item:number):void=> { console.log(item);})//filterconst filter:any=(array:number[],fn:any):number[]=>{ let filters=[] for(let i=0;i<array.length,i++;){ if(fn(array[i])){ filters.push(array[i]) } } return filters}let a=[1,3,21,312,3,1]filter(a,(item:number):boolean=>{ return item%2===0})框架中也很常见
redux中的compose
装饰器
2.3 闭包
闭包的概念:**函数A中,有一个函数B,函数B中可以访问函数A中定义的变量或者数据,此时形成了闭包(这句话暂时不严谨)
闭包的模式:分为函数模式的闭包和对象模式的闭包(函数中有一个对象)
闭包的作用:缓存数据,延迟作用域链,闭包使用的父函数的变量或参数会被永久保存,直到页面关闭
闭包的优点和缺点:缓存数据(优点也是缺点,没有及时的释放数据)
局部变量在函数中被函数使用后会被自动的释放,而闭包后,里面的局部变量的使用作用域链就会被延长
闭包的形成条件:
如果满足以下两点,那么可以把内部函数称为闭包
函数嵌套函数
内部函数使用父函数的变量或参数
详见js 9.13
3、函数式编程
3.1纯函数
3.1.1纯函数的定义是:
如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。
该函数不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变(mutation)。
如果一个函数符合上述 2 个要求,它就是纯函数
3.1.2纯函数的副作用
纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
数组的 slice 和 splice 分别是:纯函数和不纯的函数
slice 返回数组中的指定部分,不会改变原数组
splice 对数组进行操作返回该数组,会改变原数组
//不纯的let mami =18function checkAge(age:number):boolean{ return age>=mini}//纯的function checkAge(age:number):boolean{ let mami =18 return age>=mini}副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部 的状态就无法保证输出相同,就会带来副作用。 副作用来源: 配置文件 数据库 获取用户的输入 ……
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作 用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控 范围内发生。
3.2柯里化
3.2.1柯里化 (CURRYING)的定义:
当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变) 然后返回一个新的函数接收剩余的参数,返回结果
// 普通纯函数function checkAge (min, age) { return age >= min}checkAge(18, 24)checkAge(18, 20)checkAge(20, 30)// 柯里化function checkAge (min) { return function (age) { return age >= min}}// ES6 写法let checkAge = min => (age => age >= min)let checkAge18 = checkAge(18)let checkAge20 = checkAge(20)checkAge18(24)checkAge18(20)3.2.2总结
柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
这是一种对函数参数的'缓存'
让函数变的更灵活,让函数的粒度更小
可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
3.3管道
管道其实就是代表了函数处理数据的过程,例如fn被输入了a,传出了b,这中间数据处理的过程就是管道。

当fn函数复杂的时候,我们一般会将其粒度化(生成多个小fn),每次产生新的fn都会将上一个处理的值传入,最终再输出成b

3.4函数组合
3.4.1函数组合的概念
函数组合(compose):如果一个函数较为复杂,需要经过多个函数处理才能能到理想值。这个时候就可以把中间过程的函数合并成一个函数
合并成的函数就像是一个管道
函数组合默认从右到左执行
4、工具库的使用
4.1lodash
4.1.1安装
$ npm i --save lodash4.1.2引入
// Load the full build.var _ = require('lodash');// Load the core build.var _ = require('lodash/core');// Load the FP build for immutable auto-curried iteratee-first data-last methods.var fp = require('lodash/fp'); // Load method categories.var array = require('lodash/array');var object = require('lodash/fp/object'); // Cherry-pick methods for smaller browserify/rollup/webpack bundles.var at = require('lodash/at');var curryN = require('lodash/fp/curryN');4.1.3使用
FIRST、LAST
获取字符串或者数组的首项||尾项
const array = ['jack', 'tom', 'lucy', 'kate']console.log(_.first(array));console.log(_.last(array));TOUPPER
转为大写
const array = ['jack', 'tom', 'lucy', 'kate']console.log(_.toUpper('dasdas')); console.log(_.toUpper(_.first(array))); //组合调用MEMOIZE
记忆函数、只计算一次,后续不发生改变则不计算。缓存。
const _ = require('lodash')function getArea (r) { console.log(r) return Math.PI * r * r}// let getAreaWithMemory = _.memoize(getArea)模拟实现
function curry (func) { return function curriedFn(...args) { // 判断实参和形参的个数 if (args.length < func.length) { return function () { return curriedFn(...args.concat(Array.from(arguments))) } } return func(...args) }}CURRY
const _ = require('lodash')function getSum (a, b, c) { return a + b + c}const curried = _.curry(getSum)console.log(curried(1, 2, 3))模拟实现
// 函数的柯里化function checkAge (min) { return function (age) { return age >= min }}// ES6let checkAge = min => (age => age >= min)let checkAge18 = checkAge(18)let checkAge20 = checkAge(20)console.log(checkAge18(20))console.log(checkAge18(24))MATCH
const match = _.curry(function (reg, str) { return str.match(reg) })const haveSpace=match(/\s+/g)const haveNumber=match(/\d+/g)FILTER
const filter=_.curry(function(fn,array){ return array.filter(fn)})console.log(filter(haveSpace, ['John Connor', 'John_Donne']))COMPOSE
// lodash 中的函数组合的方法 _.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']))模拟实现
const reverse = arr => arr.reverse()const first = arr => arr[0]const toUpper = s => s.toUpperCase()function compose(...args){ return function(value){ return args.reverse().reduce((cur,pre)=>{ return pre(cur) },value) }}const compose=(...args)=>value=>args.reverse().reduce((cur,pre)=>{ return pre(cur)},value)DEBUG
/ 函数组合 调试 // NEVER SAY DIE --> never-say-dieconst _ = require('lodash')// const log = v => {// console.log(v)// return v// }const trace = _.curry((tag, v) => { console.log(tag, v) return v})// _.split()const split = _.curry((sep, str) => _.split(str, sep))// _.toLower()const join = _.curry((sep, array) => _.join(array, sep))const map = _.curry((fn, array) => _.map(array, fn))const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' '))console.log(f('NEVER SAY DIE'))函数式编程FP
// lodash 的 fp 模块// NEVER SAY DIE --> never-say-dieconst fp = require('lodash/fp')const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))console.log(f('NEVER SAY DIE')).
5、pointfree
5.1、定义
oint Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参 数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数
// 非 Point Free 模式// Hello World => hello_worldfunction f (word) { return word.toLowerCase().replace(/\s+/g, '_');}// Point Freeconst fp = require('lodash/fp')const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)console.log(f('Hello World'))5.2、使用 Point Free 的模式,把单词中的首字母提取并转换成大写
const fp = require('lodash/fp')const firstLetterToUpper = fp.flowRight(join('. '),fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))console.log(firstLetterToUpper('world wild web'))// => W. W. W6、函子
6.1函子的定义
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运 行一个函数对值进行处理(变形关系)
class Container { static of (value ){//一个静态方法,new关键字创建一个含子 return new Container(value) } constructor(value ){ this._value=value } // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器 map(fn){ return Container.of(fn(this._value)) }}6.2maybe函子
MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
class MayBe { static of (value) { return new MayBe(value)} constructor (value) { this._value = value} // 如果对空值变形的话直接返回 值为 null 的函子 map (fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))} isNothing () { return this._value === null || this._value === undefined}}// 传入具体值MayBe.of('Hello World').map(x => x.toUpperCase())// 传入 null 的情况MayBe.of(null).map(x => x.toUpperCase())// => MayBe { _value: null }6.3Either函子
Either 两者中的任何一个,类似于 if...else...的处理
异常会让函数变的不纯,Either 函子可以用来做异常处理
class Left { static of(value) { return new Left(value) } constructor(value) { this._value = value } map(fn) { return this }}class Right { static of(value) { return new Right(value) } constructor(value) { this._value = value } map(fn) { return Right.of(fn(this._value)) }}function ParseJson(str) { try { return Right.of(JSON.parse(str)) } catch (e) { return Left.of(e) }}// let r = ParseJson('{ name: zs }')// console.log(r) /*=>functore.js:92 Left {_value: SyntaxError: Unexpected token n in JSON at position 2 at JSON.parse (<anonymous>) at ParseJ…}*/let r = parseJSON('{ "name": "zs" }') .map(x => x.name.toUpperCase())console.log(r)6.4IO函子
IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操 作纯
IO函子返回的是一个IO函子
把不纯的操作交给调用者来处理
class IO{ static of (value){ return new IO( function(){ return value } ) } constructor(fn){ this._value=fn } map(fn){ // 把当前的 value 和 传入的 fn 组合成一个新的函数 return new IO(fp.flowRight(fn,this._value)) }}// 调用let r = IO.of(process).map(p => p.execPath)// console.log(r)console.log(r._value())6.5Task异步
异步任务的实现过于复杂,我们使用 folktale 中的 Task 来演示
folktale 一个标准的函数式编程库
和 lodash、ramda 不同的是,他没有提供很多功能函数只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等
const { compose, curry } = require('folktale/core/lambda')const { toUpper, first } = require('lodash/fp')// 第一个参数是传入函数的参数个数let f = curry(2, function (x, y) { console.log(x + y)})f(3, 4)f(3)(4)// 函数组合let f = compose(toUpper, first)f(['one', 'two'])function readFile(fileName) { return task(resolver => { fs.readFile(fileName, 'utf-8', (err, data) => { if (err) resolver.reject(err) resolver.resolve(data) }) })}readFile('../package.json') .map(fp.split('\n')) .map(fp.find(x => x.includes('version'))) .run() .listen({ onRejected: err => { console.log(err) }, onResolved: value => { console.log(value) } })
6.6Pointed函子
Pointed 函子是实现了 of 静态方法的函子
of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理值)
6.7Monad 单子
上面已经了解过IO函子,其最终返回的仍然是要给函子,如果我们要在IO函子内使用多个方法就会进入类似金字塔层级的代码。
在使用 IO 函子的时候,如果我们写出如下代码:
const fs = require('fs')const fp = require('lodash/fp')let readFile = function (filename) { return new IO(function() { return fs.readFileSync(filename, 'utf-8')})}let print = function(x) { return new IO(function() { console.log(x) return x})}// IO(IO(x))let cat = fp.flowRight(print, readFile)// 调用let r = cat('package.json')._value()._value()console.log(r)Monad 函子是可以变扁的 Pointed 函子,IO(IO(x)) 一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
const fp = require('lodash/fp')// IO Monadclass IO { static of (x) { return new IO(function () { return x })} constructor (fn) { this._value = fn} map (fn) { return new IO(fp.flowRight(fn, this._value))} join () { return this._value()} flatMap (fn) { return this.map(fn).join()}}let r = readFile('package.json') .map(fp.toUpper) .flatMap(print) .join()6.8总结
函数式编程的运算不直接操作值,而是通过函子完成
函子就是实现了map链式调用的对象
函子就是一个盒子,对值进行了包装返回了一个类
如果需要处理盒子内的值,则需要通过map传递函数进行处理
函子最终返回了一个新的函子
6.9相关资料
结语
以上笔记来自:个人整理、网路收集、拉勾教育大前端高薪训练营
文章中可能会有很多错误,如果出现了错误请大家多多包涵指正(/*手动狗头保命*/),我也会及时修改,希望能和大家一起成长。
下一章,函数性能优化