第一部分:函数式编程,柯里化
一,首先我们为什么要学习函数式编程
-
函数式编程是随着React的流行受到越来越多的关注。
-
函数式编程可以抛弃this。
-
Vue3也开始拥抱函数式编程。
-
打包过程中可以更好的利用tree shaking过滤无用代码。
-
方便测试,方便并行处理。
-
有很多库可以帮助我们进行函数式开发:lodash,underscore,ramda。
二,什么是函数式编程
//非函数式
let num1 = 2
let num2 = 3
let num3 = num1 + num2
console.log(num3) //5
//函数式
function add(num1,num2){
retuen num1 + num2
}
let sum = add(2,3)
console.log(sum) //5
三,为什么说函数是一等公民(学习函数式编程之前需要先了解的概念)
-
因为函数可以存储在变量中。
// 把函数赋值给变量 let fn = function () { console.log("Hello First-class Function") }; -
因为函数作为参数(高阶函数)。
//模仿foreach function forEach(array, fn) { for (let i = 0; i < array.length; i++) { fn(array[i]); } } let arr1 = [1, 3, 5, 7, 9]; forEach(arr1, (item) => console.log(item)); //模拟filter function filter(array, fn) { let results = []; for (let i = 0; i < array.length; i++) { if (fn(array[i])) { results.push(array[i]); } } return results; } let arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]; let a = filter(arr2, (item) => { return item % 2 === 0; }); console.log(a); -
因为函数作为返回值(高阶函数)。
//基本演示 function makeFn() { let msg = "Hello function"; return function () { console.log(msg); } } // 调用方法一 let fn = makeFn(); fn(); //调用方法二 makeFn()(); //once只调用一次函数 function once(fn) { let done = false; return function () { if (!done) { done = true; // console.log(this); // console.log(arguments); // return fn.apply(this, arguments); return fn.apply(this, arguments); } }; } let pay = once(function (money) { console.log(`支付了${money} RMB`); }); //只会调用第一个 pay(20); pay(20); pay(20); pay(25); -
常用高阶函数。
//模仿map const map = (array, fn) => { let results = []; for (let value of array) { results.push(fn(value)); } return results; }; let arr1 = [1, 3, 5, 7, 9]; arr = map(arr1, (v) => v * v); console.log(arr); //模仿every const every = (array, fn) => { let results = true; for (let value of array) { results = fn(value); if (!results) { break; } } return results; }; let arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]; let r = every(arr2, (value) => value < 10); console.log(r); //模仿some const some = (array, fn) => { let results = false; for (let value of array) { results = fn(value); if (results) { break; } } return results; }; let arr3 = [1, 2, 3, 4, 5, 6, 7, 8, 9]; let rr = some(arr3, (v) => v % 2 === 0); console.log(rr);
四,闭包
-
闭包的概念:函数和其周围的状态(语法环境)的引用捆版在一起形成闭包。
-
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域的成员。
-
闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈移除,但是堆上的作用域成员因为把外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
//函数作为返回值---下面的代码使用了闭包 function makefn() { let msg = "Hello function"; return function () { console.log(msg); }; } const fn = makefn; fn(); //函数作为返回值---下面的代码使用了闭包 function once(fn) { let done = false; return function () { if (!done) { done = true; return fn.apply(this, arguments); } }; } let pay = once(function (money) { console.log(`支付了${money}RMB`); });
五,纯函数(纯函数的概念及纯函数库)
-
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用(副作用后面讲)。
-
出函数就类似数学中的函数(用来描述输入和输出之间的关系)y=f(x)。
-
lodash是一个纯函数的功能库,提供了对数组,对象,字符串,函数等操作的一些方法。
数组的slice和splice分别是:纯函数和不纯函数 1.slice返回数组中指定的部分,不会改变原数组 2.splice对数组进行操作返回数组,会改变原数组 //下面三次console.log输出结果相同,因此slice是纯函数(因为满足相同的输入返回相同的输出) let array = [1, 2, 3, 4, 5, 6, 7, 8, 9]; console.log(array.slice(0, 5)); console.log(array.slice(0, 5)); console.log(array.slice(0, 5)); //下面二次console.log输出结果不相同,因此splice不是纯函数(因为不满足相同的输入返回相同的输出) console.log(array.splice(0, 5)); console.log(array.splice(0, 5));
六,lodash函数库的基本使用(我是在node环境运行)
lodash函数库中文文档:www.lodashjs.com/
//first方法可以获取数组 array 的第一个元素。
const _ = require("lodash");
let arr = ["app", "bpp", "cpp", "dpp"];
console.log(_.first(arr));
七,使用纯函数的好处
-
可缓存: 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
-
可测试: 纯函数让测试更方便
-
并行处理在多线程环境下进行操作共享的内存数据很可能会出现意外情况纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web worker)
//可缓存 //基本运算函数 function getArea(r) { console.log("再次调用"); return Math.PI * r * r;} //lodash函数库中的记忆函数memoize const _ = require("lodash");let getAreaWithMemory = _.memoize(getArea); //以下打印因为输入相同参数,并且返回相同的结果,所以getArea的console.log不会被调用 console.log(getAreaWithMemory(4)); //50.26548245743669 console.log(getAreaWithMemory(4)); //50.26548245743669 console.log(getAreaWithMemory(4)); //50.26548245743669 //模拟momeize记忆函数方法 function memoize(fn){ let cache = {} // 缓存结果 return function (){ //先获取参数 let key = JOSN.stringify(arguments) //判断有没有缓存结果,有的话获取结果,没有的话执行结果 cache[key] = cache[key] || fn.apply(fn , arguments) return cache[key] } } let getAreaWithMemory1 = memoize(getArea) console.log(getAreaWithMemory1(4)); //50.26548245743669 console.log(getAreaWithMemory1(4)); //50.26548245743669 console.log(getAreaWithMemory1(4)); //50.26548245743669
八,纯函数的副作用
let mini = 18; function checkAge(age) { return age >= mini; }
-
副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
-
所有的外部文件交互都可能代理副作用,副作用也使得方法通用性下降不适合扩展和可重复性。同时副作用会给程序带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控的范围内发生
-
1.配置文件2.数据库3.获取用户的输入
九,柯里化
- 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永久不变)
- 然后返回一个新的函数接收新的函数接收剩余参数,返回结果
lodash中柯里化函数
柯里化函数案例
十,模拟lodash柯里化(curry)函数原理
//lodash--curry柯里化原理
function curry(fn){
return function curriedFn(...agrs){
//如果传入参数小于函数规定参数数量
//传入参数数量可以通过函数名+length的方式获取
if(agrs.length < fn.length){
retuen function(){
//返回函数并且把所有参数拼接
return curriedFn(...agrs.concat(Array.from(arguments)))
}
}
//传入参数于规定参数一致
return fn(...agrs)
}
}
//测试
function getNum(a, b, c){
return a + b + c
}
const curried = curry(getNum)
console.log(curried(1, 2, 3))
console.log(curried(1)(2, 3));console.log(curried(1, 2)(3));
十一,柯里化(curry)总结
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的'缓存'
- 让函数变得更灵活,让函数的粒度更小
- 可以把多元函数(多个参数)转换成一元函数(一个参数),可以结合使用函数产生强大的功能
十二,组合函数
- 纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
- 比如用lodash函数库中的方法获取数组中的最后一个元素在转换成大写字母:.toUpper(.frist(_.reverse(array)))
- 函数组合可以让我们把细粒度的函数重新组合生成以一个新的函数
- 函数组合:如果一个函数要经过多个函数处理才得到最终值,这个时候把中间过程的函数合并成一个函数
- 函数就像是数据管道,函数组合就是把这些管道链接起来,让数据穿透过管道形成最终结果
- 函数组合默认是从右到左执行
十三,lodash中组合函数
- lodash中的组合函数flow()或者flowRight(),他们可以组合多个函数
- flow()是从左到右运行
- flowRight()是从右到左运行
十四,模拟lodash中flowRight(原理)
function compose(...agrs){
return function(value){
return args.reverse().reduce(function(acc,fn){
//acc代表值的汇总
//fn代表传入的每个值(函数)
return fn(acc)
},value)
//value是acc的初始化值,也就是后面r函数传入的值
}
}
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const r = compose(toUpper, first, reverse)
console.log(r(['one','two','three'])) //THREE
//es6写法
const compose_es6 = (...args) => (value) =>
args.reverse().reduce((acc,fn) => fn(acc) , value)
const reverse = (arr) => arr.reverse();
const first = (arr) => arr[0];
const toUpper = (s) => s.toUpperCase();
const r_es6 = compose_es6(toUpper, first, reverse);
console.log(r_es6(["one", "two", "three"])); //THREE
十四,函数组合的调试
方法一(缺点不是定位,当打印多个调试函数是,无法得知是哪一个函数打印的结果)
const _ = require("lodash");
//调试函数
const log = v =>{
console.log(v)
return v
}
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep ,array) => _.join(array, fn))
const map = _.curry((fn , array) => _.map(array, fn))
const f = _.flowRight(
join("-"),
log,
map(_.toLower),
log,
split(" ")
)
console.log(f('NEVER SAY DIE'))
['NEVER','SAY','DIE'] //log不确定是哪一个log
['never','say','die'] //log不确定是哪一个log
never-say-die
方法二(定位,当打印多个调试函数是,可以得知是哪一个函数打印的结果)
const _ = require('lodash')
const log = _.curry((tag,v) =>{
console.log(tag,v)
return v
})
const split = _.curry((sep, str) => _.split(str, sep));
const join = _.curry((sep, array) => _.join(array, sep));
const map = _.curry((fn, array) => _.map(array, fn))
const f = _.flowRight(
join("-"),
log("map 之后"),
map(_.toLower),
log("split 之后"),
split(" "));
console.log(f("NEVER SAY DIE"));
//输出结果
split 之后 [ 'NEVER', 'SAY', 'DIE' ]
map 之后 [ 'never', 'say', 'die' ]
never-say-die
十五,lodash中的fp模块
- lodash的fp模块提供了使用的,对函数式编程友好的方法
- 提供了不可变auto-curried iteratee-first data-last 的方法(已经柯里化,函数优先,数据之后)
十六,lodash中map方法的问题
//lodash
const _ = require('lodash')
//lodash的map函数接收3个参数(value, index | key , collection)
_.map(['23','8','10'] , parseInt) //[23, NaN , 10]
//parseInt接收3个参数
//parseInt的第二个参数是2-36,0的话是按照十进制
所以上面的方法返回
parseInt('23', 0, array)
parseInt('8', 1, array)
parseInt('10', 2, array)
//lodash/fp模块
const fp = require('lodash/fp')
fp模块的map不会出现和上面一样的情况
因为map函数只接收一个参数(当前要处理的元素)
fp.map(parseInt, ['23','8','10'])
fp.map(parseInt)(['23','8','10'])
第二部分:异步编程
一,promise
- promise就是一个类,在执行这个类的时候,需要传递一个执行器进去,执行器会立即执行(也就是resolve和reject其中之一),一旦调用就无法改变
- promise中有三种状态,分别为成功fulfilled,失败rejected,等待pending(调用reject等于把pending改为rejected)(调用resolve等于把pending改为fulfilled)
- resolve和reject函数是用来改变状态的 resolve:fulfilled reject:rejected
- then方法内部的做法是判断状态,如果状态是成功,调用成功回调函数,如果状态是失败,调用失败回调函数,then方法是被定义在原型对象中的
- then成功回调有一个参数,表示之后的只,then失败回调函数有一个参数,表示失败的原因
- then方法是可以被链式调用的,后面的then方法的回调函数拿到的值是上一个then方法的回调函数的返回值
- 链式调用中的then方法是不允许返回当前then方法的promise对象的
- then的链式调用如果前面的then都没有回调函数,promise的状态会依次向后传递,找到第一个带回调函数的then方法,promise的状态也可以一直向后传递,每次then方法return该状态即可
- promise,all方法是用来解决异步并发问题的,它允许我们按照异步代码的调用顺序,得到按顺序的结果。promise.all方法接收一个数组,数组里面可以存放任何值,promise.all返回值也是一个promise对象,因此后面可以用then,并且promise是一个静态方法
- promise.race和promise.all差不多,promise.ll等待所有任务结束,同步执行多个请求,有一个出错就报错,promise.race只会等待第一个结束的任务
- finally方法,无论promise方法是否成功还是失败,finally方法中的回调函数始终会被回调一次,finally后面可以链式调用then方法
二,promise的基本用法
const promise = new Promise ((resolve , reject) =>{
//只能回调一个,一旦回调就无法改变
resolve('成功回调')
reject('失败回调')
})
//then里面包含两个参数,一个成功回调,一个失败回调
promise.then(
res => console.log(res),
err => console.log(err)
)
或者
promise.then(
res => console.log(res)
)
.catch(
err => console.log(err)
)
两种方法的区别是第一种不能捕获异常,第二种可以捕获异常
三,promise的静态方法
//promise.resolve
let r = Promise('foo')
r输出的是一个promise对象
这个等价于上面的r
Promise.resolve('foo').then(
res => console.log(res),
err => console.log(err)
)
//promise.reject
Promise.reject(new Error('err')).catch( err => console.log(err))
四,promise的并行执行方法
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseText = "json";
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response);
} else
{
reject(new Error(this.statusText));
}
};
xhr.send();
});
}
//本地文件创建一个json文件
//promise.all 同步执行多个请求的·方法,有一个出错就出错
//正确的方法
var promise = Promise.all([
ajax("/asyn/user.json"),
ajax("/asyn/user.json"),
]);
//错误的方法
var promise = Promise.all([
ajax("/asyn/user.json"),
ajax("/asyn/ussssser.json"),
]);
//promise.race 只会等待第一个结束的任务(执行时间快的)
const request = ajax("/asyn/user.json");
const timeout = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("not"));
}, 500);
});
// 如果request执行完成则返回,为完成则输出catch
Promise.race([request, timeout])
.then((res) => console.log(res))
.catch((err) => console.log(err));
五,Generator
//创建一个生成器函数
function* foo() {
console.log("start");
}
//调用时并不会立即执行,而是得到一个生成器对象
const generator = foo();
//手动调用才会执行,此时打印console
generator.next();
// ====================
function* foo2() {
try {
//也可以通过yield关键字向外传递值,yield关键字可以暂停生成器执行,在下一次next方法才会继续执行
const res = yield "foo";
// 由于下面通过generator2.next("bar")传入参数, 所以res为bar;
console.log(res);
} catch (e) {
console.log(e);
}
}
const generator2 = foo2();
const result = generator2.next();
console.log(result);
//{value:'foo',done:false}
generator2.next("bar");
//继续执行field后面
//也可以传入参数替换yield关键字的参数,此时关键子yield的参数变为bar
generator2.throw(new Error("error"));
六,async & await
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseText = "json";
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
xhr.send();
});
}
async function main() {
try {
const users = await ajax("/asyn/user.json");
console.log(users);
const users2 = await ajax("/asyn/user.json");
console.log(users2);
} catch (e) {
console.log(e);
}
}
main();