函数式编程在前端已经成为了一个热门的话题,近几年很多的应用程序代码库里大量使用着函数式编程思想。这里对JavaSctipt中的函数式编程做一个简单介绍。
什么是函数式编程
函数式编程是一种编程范式,主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果。函数式编程意味着开发者可以在更短的时间内编写具有更少错误的代码。
函数式编程的简单例子
假设要把一个字符串转换成每个单词首字母大写,可以这样来实现:
var string = 'i do like yanggb'; var result = string .split(' ') .map(v => v.slice(0, 1).toUpperCase() + v.slice(1)) .join(' ');
在这个例子中,为了得到想要的结果,先调用split()方法将字符串转换成数组,然后再调用map()方法把各个元素的首字母转换成大写,最后再调用join()方法把数组转换成字符串。这里的整个过程就是join(map(split(str))),体现了函数式编程的核心思想:通过函数对数据进行转换。
函数式编程的两个基本特点
通过上面的例子可以得到函数式编程有两个基本特点:
1.通过函数来对数据进行转换。
2.通过串联多个函数来求最终结果。
与命令式编程、声明式编程的对比
这里简单对比下函数式编程与命令式编程、声明式编程的区别。
什么是命令式编程
命令式编程,就是通过编写一条又一条的指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。命令式代码中会频繁使用语句来完成某些行为,比如for、if、switch和throw等语句。
// 命令式编程 var vegetable = ['菠菜', '冬瓜', '太空椒', '番茄', '油菜']; var food = []; for (var i = 0; i < vegetable.length; i++) { food.push(vegetable[i]); }
什么是声明式编程
声明式编程,就是通过写表达式的方法来声明我们想要做什么,而不是通过一步一步的指令,相当于是对命令式编程的一种简单封装。这些表达式通常是某些函数调用的复合、一些值和操作符,用来计算出想要的结果值。
// 声明式编程 var food = vegetable.map(c => c);
函数式编程与命令式编程、声明式编程的区别
从上面的例子中可以看出,声明式的写法是一个表达式,无需关心如何进行计数器迭代,返回的数组如何收集,它指明的是做什么,而不是怎么做。函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,完全可以不考虑函数内部是如何实现的,开发者可以专注于编写业务代码。
函数式编程的特性
函数式编程有一些常见的特性。
无副作用
无副作用的特性,是指调用函数的时候不会修改外部的状态,即一个函数调用n次之后依然返回同样的结果。
var a = 1; function f1() { // 多次调用结果不一样 a++; // 含有副作用,它修改了外部变量 a return a; } function f2(a) { // 多次调用结果一样 return a + 1; // 无副作用,没有修改外部状态 }
透明引用
透明引用的特性,是指一个函数只会用到传递给它的变量以及自己内部创建的变量,不会使用到其他变量(外部变量)。这个特性与无副作用的特性相呼应。
var a = 1, b = 2; function f1() { // 函数内部使用的变量并不属于它的作用域 return a + b; } function f2(a, b) { // 函数内部使用的变量是显式传递进去的 return a + b; }
不可变变量
不可变变量的特性,指的是一个变量一旦创建完成之后,就不能再被修改,任何修改都会生成一个新的变量。使用不可变变量最大的好处是线程安全,多个线程可以同时访问同一个不可变变量,而不用担心状态不一致的问题,使得并行变得容易实现。
但是由于JavaScript原生不支持不可变变量,需要通过第三方库(比如Immutable.js和Mori等)来实现。
var obj = Immutable({a: 1}); var obj2 = obj.set('a', 2); console.log(obj); // Immutable({a: 1}) console.log(obj2); // Immutable({a: 2})
函数是一等公民
常说函数是JavaScript的一等公民,指的是函数与其他数据类型一样处于平等地位。可以将函数赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。JavaScript中的闭包、高阶函数、函数柯里化和函数组合都是围绕这一特性的应用。
常见的函数式编程模型
常见的函数式编程模型有闭包、高阶函数、函数柯里化和函数组合。
闭包(Closure)
如果一个函数引用了自由变量,那么这个函数就是一个闭包。所谓的自由变量,就是指不属于该函数作用域的变量(所有的全局变量都是自由变量)。在严格意思上,引用了全局变量的函数都是闭包。但是这种闭包并没有什么作用,因此通常意思上说的闭包是指的函数内部的函数。
闭包的形成条件可以划分为两个:
1.存在内、外两层函数。
2.内层函数对外层函数的局部变量进行了引用。
闭包的用途是可以定义一些作用域局限的持久化变量,这些变量可以用来做缓存或者计算的中间两等。
// 简单的缓存工具实现 // 在匿名函数中创造了一个闭包 const cache = (function() { const store = {}; return { get(key) { return store[key]; }, set(key, val) { store[key] = val; } } }()); console.log(cache); // {get: ƒ, set: ƒ} cache.set('a', 1); cache.get('a'); // 1
上面的代码是一个简单缓存工具的实现例子,使用匿名函数创造了一个闭包,使得store对象可以一直被引用而不会被回收。
要注意的是,持久化的变量不会被正常释放,其会持续占用内存空间,很容易造成内存的浪费,需要一些额外手动的清理机制才能释放内存,这是闭包最大的一个弊端。
高阶函数(High Order Function)
函数式编程是倾向于复用一组通用的函数功能来处理数据,通过使用高阶函数来实现。因此,高阶函数指的是以函数作为参数,或者是以函数作为返回值,又或者是既以函数作为参数又以函数作为返回值的函数。
高阶函数使用的场景:
1.抽象或隔离行为、作用、异步控制流程作为回调函数,比如promises和monads等。
2.创建可以泛用于各种数据类型的功能。
3.部分应用于函数参数(偏函数应用)或创建一个柯里化的函数,用于复用或函数复合。
4.接受一个函数列表并返回一些由这个列表中的函数组合的复合函数。
JavaScript是原生支持高阶函数的,例如Array.prototype.map、Array.prototype.filter和Array.prototype.reduce。使用这些高阶函数能让我们的代码变得清晰简洁。
函数柯里化(Function Currying)
函数柯里化其实是高阶函数的一个特殊应用,又被称为部分求值。柯里化函数会接收一些参数,然后不会立即求值,而是继续返回一个新的函数,并将传入的参数通过闭包的形式保存,等到被真正求值的时候,再一次性使用所有传入的参数进行求值。
普通函数:
function add(x, y){ return x + y; }
函数柯里化:
var add = function(x) { return function(y) { return x + y; }; }; var increment = add(1); increment(2); // 3
这里定义了一个add()函数,它接收一个参数并返回一个新的函数。在调用了add()函数之后,返回的函数就会以闭包的形式保存add()函数中传递的参数。
柯里化函数:
function curryIt(fn) { // 参数fn函数的参数个数 var n = fn.length; var args = []; return function(arg) { args.push(arg); if (args.length < n) { return arguments.callee; // 返回这个函数的引用 } else { return fn.apply(this, args); } }; } function add(a, b, c) { return [a, b, c]; } var c = curryIt(add); var c1 = c(1); var c2 = c1(2); var c3 = c2(3); console.log(c3); // [1, 2, 3]
由此我们可以看出,函数柯里化是一种预加载函数的做法,具体是通过传递较少的函数得到一个已经记住了这些参数的新函数。从某种意义上来将,这是一种对参数进行缓存的做法,是一种非常高效的编写函数的方法。
函数组合(Function Composition)
前面提到过,函数式编程的一个特点是通过串联函数来求值。然而随着串联函数数量的增加,代码的可读性会不断下降。函数组合就是用来解决这个问题的一个方案。假设有一个compose()函数,它可以接收多个函数作为参数,然后返回一个新的函数。这样,当我们为这个新的函数传递参数的时候,这些参数就会在其中的函数间传递,最终返回想要的结果。
// 两个函数的组合 var compose = function(f, g) { return function(x) { return f(g(x)); }; }; // 或者 var compose = (f, g) => (x => f(g(x))); var add1 = x => x + 1; var mul5 = x => x * 5; // 调用 compose(mul5, add1)(2); // 15
"反正做不到的,随便说说也不要紧。"