六. JS纯函数_ 柯里化_ 组合
6.1. 认识JS纯函数
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
- 在react开发中纯函数是被多次提及的;
- 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
- 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
纯函数的维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
- 此函数在输入相同的值时,需产生相同的输出。
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
当然上面的定义会过于的晦涩,所以我简单总结一下:
-
确定的输入,一定会产生确定的输出;
-
函数在执行过程中,不能产生副作用;
- 比如函数在执行时,修改了外部的变量,就是副作用
-
函数的输出不依赖参数或者全局变量等;
6.2. 副作用的理解
副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;
- 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
纯函数在执行的过程中就是不能产生这样的副作用:
- 副作用往往是产生bug的 “温床” 。
纯函数的一些例子
function foo(num1, num2) {
return num1 * 2 + num2 * 2
}
[1,2,3,4].splice(0,2)
function foo(info) {
return {
...info,
age: 100
}
}
var obj = { name: "why", age: 18 }
foo(obj)
非纯函数的一些例子
// 修改了全局变量
let name = "why"
function foo() {
name = "aaa"
}
// 修改了原数组
[1,2,3,4].slice(0,2)
// 修改了参数
function foo(info) {
info.name = "aaa"
}
var obj = { name: "why", age: 18 }
foo(obj)
//输出依赖了全局变量,确定的输入产生了不确定的输出
let num1 = 5
function foo(num) {
return num1 + num
}
console.log(foo(5));
num1 = 10
console.log(foo(5));
6.3. 纯函数的优势
- 安心的编写和安心的使用;
- 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;
- 用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
6.4. 认识柯里化
维基百科的解释:
- 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;
- 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
- 柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”
维基百科的结束非常的抽象,我们这里做一个总结:
- 只传递给函数一部分参数(一个或者多个参数)来调用它,让它返回一个函数去处理剩余的参数;
- 这个过程就称之为柯里化;
// 把add()转变sum1()的过程叫做柯里化
function add(x, y, z) {
return x + 2 + y + 3 + z + 4
}
console.log(add(1, 2, 3));
function sum1(x) {
x = x + 2
return function (y) {
y = y + 3
return function (z) {
z = z + 4
return x + y + z
}
}
}
console.log(sum1(1)(2)(3));
// 简化柯里化的代码
var sum2 = x => {
x = x + 2
return y => {
y = y + 3
return z => {
z = z + 4
return x + y + z
}
}
}
console.log(sum2(1)(2)(3));
柯里化的好处
- 一个函数处理的问题单一,便于追踪排查问题,而不是将一大堆的处理过程交给一个函数来处理;
6.5. 柯里化_逻辑的复用
柯里化还有个好处是对参数进行复用,下面是两个例子
1.后面使用返回的函数时,我们不需要再继续传入count了
function makeAdder(count) {
return function (num) {
return count + num
}
}
var sum = makeAdder(5)
sum(10)
sum(22)
sum(34)
2.加入需求是打印日志(时间、类型、信息)
普通函数实现如下(每次都需要传入三个参数)
function log(date, type, message) {
console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`);
}
log(new Date(), "bug", "这个查找了bug的一些信息")
对上面函数进行柯里化
var log = date => type => message => {
return `[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`
}
var newLog=log(new Date())
newLog("feture")("这次查找了一些新增分类的信息")
newLog("update")("这次查找了一些更新的信息")
6.6. 自动柯里化函数实现
function hyCurrying(fn) {
return function curried(...args) {
// 判断接收的参数和形参个数是否一致 fn.length:add1()参数的长度
if (args.length >= fn.length) {
// 传入的参数>=形参个数时,就执行函数
return fn.apply(this, args)
} else {
// 传入的参数<形参个数时,返回一个函数继续接收参数
return function (...args2) {
// 接收到参数后,递归调用curried来检查函数的参数是否传完了
return curried.apply(this, [...args, ...args2])
}
}
}
}
function add1(x, y, z) {
return x + y + z
}
var curryAdd = hyCurrying(add1)
curryAdd(10, 20, 30)
console.log(curryAdd(10, 20)(30))
console.log(curryAdd(10)(20)(30));
下面是对hyCurrying函数的简写
function hyCurrying(fn) {
return function curried(...args) {
return args.length >= fn.length ? fn.apply(this,args) : (...args2) => curried.apply([...args, ...args2])
}
}
6.7. 自动柯里化函数内部绑定this
疑惑: 给fn绑定this有什么意义? 答:add1执行时显示绑定this
- 应用的场景:如果调用curryAdd时有绑定this,就和实际调fn的this就不一样了;
- fn.apply(this, args)实现原理:fn被调用时一定是curryAdd被调用,通过执行curryAdd来执行fn,所以要保证curryAdd和fn的this是一样的(通过curryAdd给fn绑定this),可以用apply来调用fn,把curryAdd的this传进来,这里等于是闭包
function hyCurrying(fn) {
return function curried(...args) {
return args.length >= fn.length ? fn.apply(this,args) : (...args2) => curried.apply(this,[...args, ...args2])
}
}
function add1(x, y, z) {
console.log(this.name); // why
return x + y + z + this.number // 61
}
let obj = { name: "why", number: 1 }
var curryAdd = hyCurrying(add1)
// fn的参数this是引用curried的this
console.log(curryAdd.call(obj,10, 20, 30));
//这两种方式fn的this是引用上层函数function (...args2)的this。最外层的curried的ao已经被销毁了,用断点可以验证
// 疑惑:fn执行时会向上层作用域找this,上层函数是上面的curried,他的this是window呀?
// 答:this时函数执行时确定的,和定义位置没关系,上层函数curried的AO对象由于没有形成闭包,已经被销毁了(可在(...args2)函数里用断点验证),curried.apply([...args, ...args2]函数执行时又创建的AO对象,所以fn的上层作用域是最下面的curried
console.log(curryAdd(10).call(obj,20,30));
console.log(curryAdd(10, 20).call(obj,30))
例二:curryAdd(10, 20).call(obj,30)
6.8. 认识组合函数
组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:
- 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
- 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;
- 那么是否可以将这两个函数组合起来,自动依次调用呢?
- 这个过程就是对函数的组合,我们称之为 组合函数(Compose Function);
下面实现一个最简单的组合函数
function double(num) {
return num * 2
}
function square(num) {
return num ** 2
}
function composeFn(m,n){
return function(count){
return n(m(count))
}
}
var newFn=composeFn(double,square)
console.log(newFn(10));
6.9. 实现组合函数
function double(m) {
return m * 2
}
function square(n) {
return n ** 2
}
function hycompose(...fns){
for(var i=0;i<fns.length;i++){
if(typeof fns[i] !=='function'){
throw new TypeError("请输入函数")
}
}
return function(...args){
var index =0
// 判断fns如果没有传,则直接返回;传了的话就执行第一个函数
var result = fns.length ? fns[index](...args) :args
while(++index < fns.length){
// 把第一个函数执行的结果result,给第二个函数作为参数使用
result= fns[index](result)
}
return result
}
}
var newFn=hycompose(double,square)
console.log(newFn(10));