JavaScript 函数式编程基础概念

本文探讨了函数编程中的核心概念,包括引用透明与纯函数、不可变性、等式推理、Point-free编程风格及惰性求值。通过具体示例,详细解释了这些特性如何提升代码质量和效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 引用透明(referential transparency)与纯函数

”Referential transparency means, a function call can be replaced with it's return value and not affect any of the rest of the program.” 我们拆分开来看这句话。

🎯 ”a function call can be replaced with it's return value“ 一个函数调用可以直接被其返回值替代,即函数调用等同于其返回值。这说明,无论调用多少次,只要给定的入参不变,返回值就是不变的。就是上一篇博客说到的,“same input same output”。

function add(x, y){
  return x + y
}
let result = add(1, 2) //  add(1, 2) 可以直接被替换成 3,而结果不受影响

🎯 ”not affect any of the rest of the program“ 将函数调用替换成返回值后,程序其余地方不会受到影响。这说明,这个函数调用本身不存在副作用。

var z = 0
function add(x, y){
  z ++
  return x + y
}
let result = add(1, 2) //  add(1, 2) 不可以直接被替换成 3,因为 方法调用的时候改变了全局变量 z,产生了副作用

所以,引用透明是指,一个函数的调用既符合,“same input same output”,又符合”不存在副作用“。很明显这就是我们上篇博客说的纯函数。 因此,如果一个函数存在引用透明,那么这个函数就是纯函数。

2 不可变性(Immutability)

不可变性是指在程序中,要使用不可被改变的数据。主要是为了减少程序的副作用。 这其实是针对引用类型使用过程中可能出现的bug,在我们平时开发中很常见。

例如:

let obj = { a: 1};
function add(o){
    o.b = 2
    return o;
}
let newObj = add(obj)

有开发经验的一眼就会发现这可能会有bug,因为obj作为引用类型传入函数add的时候,在add中没有被拷贝。 所以针对引用类型,我们要确保它的不可变性,确保原来的变量不被改变。

修改如下:

let obj = { a: 1};
function add(o){
   let myObj = Object.freeze(o)
    return myObj ;
}
let newObj = add(obj)

3 等式推理(equational reasoning)

一个函数返回另一个函数,并且两个函数的参数相同,可以改成下面的样子。

getPerson(function onPerson(person){
    return renderPerson(person);
});
getPerson(renderPerson)

不再需要外面包裹的函数,直接调用renderPerson作为回调函数。

例如:

Promise.resolve([1]).then((res) => console.log(res)) // [1]
Promise.resolve([1]).then(console.log) // [1]

4 Point-free 

Point-free 是指把多个函数组合在一起,不需要考虑值的传递,只考虑函数本身逻辑与运算的编程风格。

例如:

function isShort(str){
    return str.length <= 5
}
function not(fn){
    return function neg(args){
	    return !fn(args)
    }
}
not(isShort)('123456')

isShort函数用来判断字段长度是不是短。not函数用来对函数取反。两个函数组合在一起不需要在定义中提起过程中值的流动。

这里有函数组合的结构,对应在函数式编程是函数compose。为了更好地理解 Point-free,我们先来理解下 compose 和 pipe。

🎯 compose 和 pipe

compose是让函数从右到左执行,而pipe是从左到右执行函数。先看pipe,因为它从左到右的执行顺序和我们得阅读习惯更符合。

来看个题: 已知现有三个函数,求 (10+1)*8/2 后的结果。

function increment(x){
    return x + 1
}
function multi(x){
    return x * 8
}
function mod(x){
    return x / 2
}

我第一反应写成了这样令人头疼的层层嵌套的形式。

mod(multi(increment(10)))

有没有更好的办法把它扁平化处理?有的,让reduce来帮忙。

[increment, multi, mod].reduce((result, fn) => {
   return fn(result)
}, 10)

上述形式被复用为函数,就成了pipe:

function pipe(...fns){
    return function piped(arg){
        return fns.reduce((result, fn) => fn(result), arg)
    }
}

于是用 pipe 求值可以写成这样:

pipe(increment,multi,mod)(10)

compose 和 pipe 执行顺序相反,它对应的函数自然就是:

function compose(...fns){
    return function composed(arg){
        return fns.reduceRight((result, fn) => fn(result), arg)
    }
}

// 或者

function compose(...fns){
    return pipe(...fns.reverse())
}

于是用 compose 求值可以写成这样:

compose(mod, multi, increment)(10)

了解了compose 和 pipe,我们再重新回到开始的例子。它可以被写成这样更清晰、更易复用的形式。

function compose(...fns){
    return function composed(arg){
        return fns.reduceRight((result, fn) => fn(result), arg)
    }
}
function isShort(str){
    return str.length <= 5
}
function not(args){
    return !args
}
compose(not, isShort)('123456')

这样point-free风格也就更加直观,即多个函数组合在一起,只考虑函数逻辑,而不用处理值的传递。

5 惰性求值

惰性求值是指,在需要的时候才去求值,而不是页面一进来就调用函数求值。

来看代码:

function repeat1(count){
    let str = ''.padStart(count, 'a')
    return function getAs(){
        return str
    }
}
let a1 = repeat1(10)
a1()  
a1()  

第7行已经调用padStart()并给str赋值了。第8、9行只是str的获取。

如果代码第2行换成操作量大的代码,并且获取repeat1()实例后,从未被执行,或者在我还没用到的时候就执行了,就导致了性能的浪费。于是改成下面惰性求值的形式:

function repeat2(count){
    return function getAs(){
        return ''.padStart(count, 'a')
    }
}
let a2 = repeat2(10)
a2() // make the string
a2() // again

优点:避免调用不需要的函数,以免浪费性能。

缺点:每次都需要再生成一次string,如果调用量大不推荐。

针对“每次都需要再生成一次string”这个问题,在函数式编程常用库 lodash 或者 rambda 中,可有专门函数 memoize 来解决这个问题:

function repeater3(count){
  return memoize(function getAs(){
    return "".padStart(count, "a")
  }) 
}
var a3 = repeater3(10);
a3();
a3();

memoize 对于某一函数的同样的参数,不需要再计算立刻给出结果。

在函数式编程中,经常会用柯里化(currying) 和 偏函数(partial) 进行惰性求值。

🎯 currying 和 partial

currying 和 partial 都是将多个参数的函数转化成携带部分参数的函数,以此实现惰性求值。

不同的是,currying一次接受一个参数;而partial 初始预设参数,并在后面调用中获取余下的参数。

例如,要对 function ajax(url, data, cb){} 进行改造:

partial:

function ajax(url, data, cb){}
let getInfo = partial(ajax, API)
let getCurUser = partial(getInfo, {id: 1})
getCurUser(renderUser)
// 或者
getInfo({id: 1}, renderUser)

currying:

function ajax(url, data, cb){}
let ajax = curry(3, ajax)
let getInfo = ajax(API)
let getCurUser = getInfo({id: 1})
// 或者
ajax(API)({id: 1})(renderUser)

🎯 最后,做一个练习:柯里化如下代码。

[0,2,4].map(v => {
  return v + 1
})

思路演变:

function add(x, y) { return x + y; }
[0,2,4].map(v => {
    return add(v, 1);
})

// 柯里化 =>

function add(x, y) { return x + y; }
add = curry(add);
[0,2,4].map(v => {
    return add(1)(v);
})

// 等式推理 =>

function add(x, y) { return x + y; }
add = curry(add);
[0,2,4].map(add(1)) // Point-free 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值