常见的函数式编程模型
1.闭包(Closure)
闭包的概念
可以保留局部变量不被释放的代码块,被称为一个闭包。
闭包的特点:函数嵌套函数、内部函数可以引用外部函数的参数和变量、参数和变量不会被垃圾回收机制收回
// 创建一个闭包 function makeCounter() { let k = 0; return function() { return ++k; }; } const counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2
makeCounter 这个函数的代码块,在返回的函数中,对局部变量 k ,进行了引用,导致局部变量无法在函数执行结束后,被系统回收掉,从而产生了闭包。而这个闭包的作用就是,“保留住“ 了局部变量,使内层函数调用时,可以重复使用该变量;而不同于全局变量,该变量只能在函数内部被引用。
换句话说,闭包其实就是创造出了一些函数私有的 ”持久化变量“。
所以从这个例子,我们可以总结出,闭包的创造条件是:
(1)存在内、外两层函数 (2)内层函数对外层函数的局部变量进行了引用
闭包的用途
闭包的主要用途就是可以定义一些作用域局限的持久化变量,这些变量可以用来做缓存或者计算的中间量等等。
// 简单的缓存工具 // 匿名函数创造了一个闭包 const cache = (function() { const store = {}; return { get(key) { return store[key]; }, set(key, val) { store[key] = val; } } }()); cache.set('a', 1); cache.get('a'); // 1
上面例子是一个简单的缓存工具的实现,匿名函数创造了一个闭包,使得 store 对象 ,一直可以被引用,不会被回收。
闭包的弊端:持久化变量不会被正常释放,持续占用内存空间,很容易造成内存浪费,所以一般需要一些额外手动的清理机制。
2.高阶函数
接受或者返回一个函数的函数称为高阶函数,JavaScript 中见到许多原生的高阶函数,例如 Array.map , Array.reduce , Array.filter
以map函数方法为例:
map:映射是对集合而言的,即把集合的每一项都做相同的变换,产生一个新的集合
// 数组中每一项加一,组成一个新数组 // 一般写法 const arr = [1,2,3]; const rs = []; for(const n of arr){ rs.push(++n); } console.log(rs) // map改写 const arr = [1,2,3]; const rs = arr.map(n => ++n);
上面一般写法,利用 for...of 循环的方式遍历数组会产生额外的操作,而且有改变原数组的风险
而 map 函数封装了必要的操作,使我们仅需要关心映射逻辑的函数实现即可,减少了代码量,也降低了副作用产生的风险。
3 柯里化(Currying)
给定一个函数的部分参数,生成一个接受其他参数的新函数,叫做函数的柯里化。
柯里化可以使我们只关心函数的部分参数,使函数的用途更加清晰,调用更加简单。
//一般写法 function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); } // 柯里化 function currying(fn, n1, n2) { return function (m) { return fn.call(this, m, n1, n2); }; } function tailFibonacci(n, ac1, ac2){ if( n <= 1 ) {return ac2}; return tailFibonacci (n - 1, ac2, ac1 + ac2); } const Fibonacci4 = currying(tailFibonacci,1,1); Fibonacci4(100) // 573147844013817200000
4.组合
将多个函数的能力合并,创造一个新的函数,叫做组合。
// 数组中每个单词大写,做 Base64 // 一般写法 (其中一种) const arr = ['pen', 'apple', 'applypen']; const rs = []; for(const w of arr){ rs.push(btoa(w.toUpperCase())); } console.log(rs); // _.flow 改写 const arr = ['pen', 'apple', 'applypen']; const upperAndBase64 = _.partialRight(_.map, _.flow(_.upperCase, btoa)); console.log(upperAndBase64(arr));
_.flow 将转大写和转 Base64 的函数的能力合并,生成一个新的函数。方便作为参数函数或后续复用。
总结
我理解的 JavaScript 函数式编程,可能和许多传统概念不同。我并不只认为 高阶函数 算函数式编程,其他的诸如普通函数结合调用、链式结构等,我都认为属于函数式编程的范畴,只要他们是以函数作为主要载体的。
而我认为函数式编程并不是必须的,它也不应该是一个强制的规定或要求。与面向对象或其他思想一样,它也是其中一种方式。我们更多情况下,应该是几者的结合,而不是局限于概念。