入门函数式编程

在上了360的前端星计划课程后,被月影老师提出的高阶函数思想给深深吸引了,上网查了一下,好像现在正是函数式编程流行大爆发的时候,那还有什么借口不去学习呢。

个人理解

高阶函数满足的条件:函数作为参数来传递,函数作为返回值来输出。也就是处理函数并且返回函数的函数,大概长成这个样子的:

function highLevel (fn) {
	return function (...rest) {
		// 对fn做一些事
	};
}

而高阶函数是函数式编程的编程范式。

经典例子

其实在我之前不知道高阶函数这个名词的时候,我就已经见过它了,因为我见过几个经典的demo。

  1. 单例模式

    function getSingle (fn) {
    	let result;
    	return function (...rest) {
    		return result || result = fn.apply(this, rest);
    	}
    }
  2. 节流

    function throttle (fn, time) {
    	let timer;
    	return function (...rest) {
    		if (!timer) {
    			timer = setTimeout(() => {
    				fn.apply(this, rest);
    				timer = null;
    			}, time);
    		} 
    	};
    }
  3. 防抖

    function debounce (fn, time) {
    	let timer;
    	return function (...rest) {
    		clearTimeout(timer);
    		timer = setTimeout(() => {
    			fn.apply(this, rest);
    			clearTimeout(timer);
    		}, time);
    	};
    }
  4. 分时

    function timeChunk (arr, fn, count, time) {
    	let timer;
    	let deal = function () {
    		for (let i = 0; i < Math.min(count, arr.length); i++) {
    			fn(arr.shift());
    		}
    	}
    	return function () {
    		timer = setInterval(() => {
    			if (arr.length === 0) {
    				return clearInterval(timer);
    			}
    			deal();
    		}, time);
    	};
    }

函数式编程的基础

概念基础

  1. 函数是一等公民

    意思就是函数和其他基本类型变量一样,本来嘛,函数也是对象,而对象是JavaScript的基本类型之一。所以函数是一等公民。

    既然函数是一等公民,那么能直接调用就直接,不用做无意义的嵌套。

    // 比如
    function super (callback) {
    	return sub (function (a) {
    		return callback(a);
    	});
    }
    
    // 直接
    sub(callback);
  2. 纯函数

    纯函数是没有任何副作用的函数,对同样的输入只会有同样的输出。不受外界影响,也不会干扰外界(意思是纯函数除了参数是由外部决定之外,内部是不受外部变量影响的,且纯函数不会改变参数本身),严格来说就是不与函数外部环境发生任何交互。比如操作dom,log打印之类的都是属于不纯的。

    • 为什么要纯函数?

      • 可测试性: 纯函数可以很容易的进行单元测试,因为不需要构造其特定的依赖环境。
      • 可缓存性:因为纯函数只要参数不变,其输出结果一定不变,所以可以缓存。

基本方法

  1. 柯里化

    在函数式编程中柯里化是必不可少的,柯里化的作用是将多参函数分解成单参函数。

    对柯里化的好处,目前我大概理解的是单参函数方便函数的复用和函数之间的组合。

    上代码:

    let curry = function (fn) {
    	let len = fn.length,
    		args = [];
    	return function curried (...rest) {
    		args = args.concat(rest);
    		if (len > args.length) {
    			return curried;
    		}
    		return fn.apply(this, args);
    	}
    };	
    
    let testFn = function (a, b, c) {
    	return a + b + c;
    }
    
    curry(testFn)(1, 2, 3);	// 6;
    curry(testFn)(1, 2)(3); // 6;
    curry(testFn)(1)(2)(3); // 6;

    其实很简单,就是先将参数缓存着,然后够了后就开始执行,有点代理模式中缓存代理的感觉。

  2. 函数组合

    利用函数是一等公民和纯函数的特点,函数式编程中很有意思的就是函数组合。

    函数组合就是将各各纯函数以管道形式组合起来,类似一个pipe,来个例子:

    let toUpperCase = function (str) {
    	return str.toUpperCase();
    }
    
    let reverseStr = function (str) {
    	return str.split('').reverse().join('');
    } 
    
    // 组合函数
    let compose = function (fn, gn) {
    	return function (...rest) {
    		return fn(gn.apply(this, rest));
    	};
    }
    
    let getNewStr = compose(reverseStr, toUpperCase);
    
    getNewStr('hello');	//OLLEH

    这里的compose组合方式参考Ramda.js,从右到左执行循序,然后给组合函数提供一个参数'hello',然后参数通过组合函数中的每一个函数处理后输出结果。当然这里的compose只能处理两个函数,所以可以稍微扩展一下:

    let compose = function (...rest) {
    	let fns = rest,
    		len = rest.length;
    	return function () {
    		for (let i = len - 1; i >= 0; i--) {
    			let fn = fns[i],
    				args = Array.prototype.slice.call(arguments, 0, fn.length || arguments.length),
    				nextArgs = [];
    			nextArgs.push(fn.apply(this, args));
    			arguments = nextArgs;
    		}
    		return arguments[0];		
    	};
    };

    这样compose就可以处理多个函数了,可是compose的运行规则是,除了最后一个函数是接收外界参数以外,里面的每一个参数都是接收上一个函数的返回值。那如果compose参数里面想要放置多参函数怎么办呢,柯里化就来起作用了。比如:

    const testArr = ['qq', 'qwq', 'www', 'js'];
    
    let hasQ = function (str) {
    	return /q/i.test(str);
    };
    
    let filter = function (fn, arr) {
    	return arr.filter(fn);
    };
    
    let map = function (fn, arr) {
    	return arr.map(fn);
    };
    
    let getHead = function (arr) {
    	return arr[0];
    };
    
    let filterQ = curry(filter)(hasQ);
    let mapUpperCase = curry(map)(toUpperCase);
    
    let getResult = compose(getHead, mapUpperCase, filterQ);
    
    getResult(testArr);	//QQ

    上面的程序中,多参函数filter和map均被柯里化后就可以放入compose函数中运行。其实这两个多参函数还有一个特点,那就是参数中函数在前,要处理的数据在后,这样也是很理所当然的,想想看,我们使用柯里化固定的是前面的参数,固定的参数只可能是计算方法呀,哪有固定数据的,将数据参数固定后还能干什么呢。(当然也可以柯里化后面的参数,比如说在奇舞周刊里面有一期提到的bindRight,但是毕竟习惯是固定前面的参数)

    阮一峰的博客中也说过这一点,在博客里阮一峰大大极大的推崇了这种理念:"function first,data last",也就是Ramda.js所坚持的,然后提出了pointfree的概念,即函数组合只组合纯粹的计算过程。

关于函数式编程,还有一个范畴的概念,我没有很好地理解明白,感觉就是一个映射,姑且先这么理解着,以后有了新的理解再来补充。

如果这篇文章对你很有帮助,你可以犒劳一下WO

打赏