web前端高级JavaScript - JS高阶编程之模块化思想/惰性函数/柯理化函数/compose组合函数

·  阅读 326

JS高阶编程技巧之模块化思想/惰性函数/柯理化函数/compose组合函数

利用闭包机制,实现出来的一些高阶编程方式

  • 模块化思想
  • 惰性函数
  • 柯理化函数
    • 高阶组件 - React
    • 函数的防抖和节流
    • bind
  • compose组合函数
  • 模块化思想

模块化思想演变过程:单例 -> AMD(require.js) -> CMD(sea.js) -> CommonJS(Node) -> ES6Module 为什么要引入模块化思想呢? 在传统的js编程中,往往会将很多函数和变量都写在同一个js文件中,那么如果是团队协作开发,同一个 文件可能就会由多个人去维护。那么在这种情况下就很有可能造成全局变量的污染;比如小A写了一个queryData函数用于查询数据,过了一段时间小B也想要定义一个函数用来查询数据,于是也命名为queryData,但他并不知道其实已经有一个同名的函数了,那么这种情况下就造成了全局变量污染,某一个queryData必然会被覆盖掉。如何才能避免这种情况呢?

//传统js编程
function queryData(){}
//..............
function queryData(){}
复制代码

这时模块化思想就应运而生了;比如我们可以利用闭包机制定义一个自执行函数,把每一个人的代码都用自执行函数封装起来。

//模块化
//小A
(function(){
	function queryData(){
		//........
	}
	//.......
})();

//小B
(function(){
	function queryData(){
		//........
	}
	//.......
})();
复制代码

这样每个人就可以在自己的模块中使用自己定义的函数了,即使是重名函数也互不影响。但是呢,这种封装虽然避免了全局变量的污染,却同时也会引起另外一个问题:比如小B在写queryData时发现功能跟小A的queryData差不多,甚至可以拿来直接用的,但由于每个模块都是互相隔离的并不能互相访问。于是小B决定找小A讨论一下。经过一番冥思苦想,终于他们想出了一个办法,那就是将公共的函数作为全局变量window的属性开放出来

//模块化共享
//小A
(function(){
	function queryData(){
		//........
	}
	window.queryData = queryData;
	//.......
})();

//小B
(function(){
	window.queryData();
	//.......
})();
复制代码

问题解决了两个人很开心。但是好景不长,由于功能JS越来越复杂,代码越来越多,那么需要互相开放的函数也越来越多,如果仍然用window挂载的方式显然是无法满足了,因为如果挂载多了跟传统JS编程又没什么两样了,同样也会造成变量污染。这下可难坏了两个人。在偶然的一次机会中两人得知:在JS中每个对象都是一个单独的实例,那如果把需要共享出去的方法作为对象返回出去,即避免了变量污染,同时也能共享接口,岂不是一举两得

//小A
var aMoudle = (function(){
	function queryData(){
		//........
	}
	//.......
	return{
		queryData: queryData,
		//......
	}
})();

//小B
var bMoudle = (function(){
	aMoudle.queryData();
	//.......
	return {
		//....
	}
})();
复制代码

本案例是利用了闭包的机制实现了模块化思想。但是个人感觉直接利用面向对象的思想也可以解决。

  • 惰性函数

所谓的惰性函数,顾名思义就是懒函数,那到底是怎么个懒法呢,来看一下下面的场景: 小A在项目开发中经常需要去获取页面中元素的样式,同时还要考虑兼容一些老版本的浏览器,于是小A决定自己封装一个函数,专门用来获取不同元素的不同样式。

function getCss(element, attr){
	//浏览器兼容性判断
	if('getComputedStyle' in window){//或者用 !window.getComputedStyle
		return window.getComputedStyle(element)[attr];
	}else{
		return element.currentStyle[attr];
	}
}
var bodyBackground = getCss(document.body, 'background');
var bodyHeight= getCss(document.body, 'height');
var bodywidth= getCss(document.body, 'width');
复制代码

小A 很开心,但用着用着,小A发现:虽然获取元素样式被封装成了方法也能兼容不同版本的浏览器,但是如果需要一次获取多个样式,那么就需要多次调用getCss函数,同时就需要多次进行兼容性判断,然而这期间并没有切换浏览器 ,这样的话其实判断一次就可以了,不需要每次都判断。于是小A就想:如果第一次执行完getCss函数也判断出了浏览器兼容性,那在不改变调用代码的情况下有没有什么办法可以让getCss的代码体变一下呢?于是:

function getCss(element, attr){
	//浏览器兼容性判断
	if('getComputedStyle' in window){//或者用 !window.getComputedStyle
		getCss = function(element, attr){
			return window.getComputedStyle(element)[attr];
		} 
	}else{
		getCss = function(element, attr){
			return element.currentStyle[attr];		
		}
	}
	//第一次调用时需要将结果返回
	return getCss(element, attr);
}
var bodyBackground = getCss(document.body, 'background');
var bodyHeight= getCss(document.body, 'height');
var bodywidth= getCss(document.body, 'width');
复制代码

这样在第一次调用getCss时会进行兼容性判断,那么不管是否兼容,getCss都会重新指向另一个小函数,在这个小函数中就不再进行兼容性判断了,而且后面再去调用getCss时也就是直接调用了getCss的新的指向(小函数)。这就是所谓的惰性函数思想。

  • 柯理化函数

所谓的柯理化函数:是一种预先处理的思想,那什么又是预先处理呢?其实就是利用闭包机制,把一些信息预先存储起来,以后基于作用域链,访问到事先存储的信息,然后进行相关的处理。所有符合这种模式(或闭包应用的)都被称为柯理化函数。 下面来看一个场景:

  • 首先我想调用一个函数a,传给它一个数字
  • 然后把这个函数的返回值再作为一个函数b进行调用,并且传给它不定个数的数字作为参数。
  • 最后把第一次调用函数a时传递的数字和第二次调用函数b是时传递的数字进行相加求和。
//x是预先存储的值
function a(x){
	return ...
}
var b = a(10);
var total = b(20,30); // '10+20+30'
console.log(total);
total = b(20,30, 40);//'10+20+30+40'
console.log(total);
复制代码

上面的函数a该如何实现才能满足呢?在前面我们学过的闭包机制就能很好的解决。

  • 既然能把a的返回值作为函数调用,那么a的返回值必然也是一个函数。
  • 需要注意的是:==返回的函数的参数是不定个数的==这里可以使用函数自带的类数组:arguments或者使用ES6的 ==...== 操作符。
function carring(x){
	return function(...args){
		//args是一个数组,首先我们需要将预先保存的x的值添加到数组中,然后再对数组进行求和
		args.unshift(x);
		//数组求和 - 循环
		var total = 0;
		args.forEach(function(item, index){
			total += item;
		});		
		//数组求和 - reduce
		total = args.reduce(function(result, item, index){
			return result + item;
		})
	}
}
var b = a(10);
var total = b(20,30); // '10+20+30'
console.log(total);
total = b(20,30, 40);//'10+20+30+40'
console.log(total);
复制代码
  • 数组reduce方法的扩展

数组的reduce方法:在遍历数组的过程中,可以累积上一次处理的结果,基于上次处理的结果继续遍历处理

arr.reduce([callback], [initialValue])
var arr = [10, 20, 30, 40];
var res = arr.reduce(function(result, item, index){
	//initialValue 初始值不传递,result默认初始值是数组的第一项,然后reduce从数组的第二项开始遍历
	//每遍历数组中的一项,回调函数就被触发执行一次
	// + result 存储的是上一次回调函数返回的结果(除了第一次是初始值或数组第一项)
	// + item 当前遍历的这一项
	// + index 当前遍历的这一项的索引
	return item + result;
});

arr.reduce(function(result, item, index){
	//如果传递初始值,则result第一次的结果就是初始值,item从数组的第一项开始遍历
	return result + item;
}, 0);

///======reduce的内部实现原理
Array.prototype.reduce = function(callback, initial){
	var self = this, i = 0;
	//判断callback是否是一个函数
	if(typeof callback !== 'function') throw new TypeError('callback is not a function')
	//初始参数未传,则把数组一项值赋给initial,并让数组从第二项开始遍历
	if(typeof initial === undefined){
		initial = self[0];
		i = 1;
	}
	
	for(;i<self.length; i++){
		var item = self[i], index = i;
		initial = callback(initial, item, index);//将callback的返回结果赋给initial
	}
	return initial;
}
复制代码
  • compose组合函数

在函数式编程中一个很重要的概念就是函数组合,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果。或者可以理解为:将多个函数组合成一个函数同时完成数据的传递。 看如下场景:

  • 有加(add)、减(sub)、乘(mul)、除(div)四个运算函数
  • 现在我想先调用加法函数add,得到加法结果
  • 然后将加法运算得到的结果传递给减法函数,得到减法结果
  • 再将减法运算的结果传递给乘法运算,得到乘法结果
  • 最后再将乘法结果传递给除法运算,得到最终的结果

上述过程就实现了函数的连接以及数据的传递,看下面代码的实现方式:

var add = x => x + 10;
var sub = x => x - 3;
var mul = x => x * 8;
var div = x => x / 2;
var addRes = add(5);//加法运算
var subRes = sub(addRes);//将加法运算的结果进行减法运算
var mulRes = mul(subRes);//将减法运算的结果进行乘法运算
var divRes = div(mulRes);//将乘法运算的结果进行除法运算
//上述过程合并
var res = div(mul(sub(add(5)))); //48
复制代码

上述代码虽然能够实现我们期望的结果,但如果要有很多个函数,明显可读性变得很差。接下来我们就通过构建一个compose函数来实现上述过程。 我们要实现的compose函数的特点:

  • 接受任意多个函数作为参数
  • 传递的每个函数都只接收一个参数
  • 返回值也是一个函数

简而言之:compose可以把类似于f(g(h(x)))这种写法简化成(f,g,h)(x)。下面是改造后的代码:

//funcs:存储的是最后需要执行的函数及顺序(最后传递的函数最先执行)
// 执行compose只是把最后要执行的函数及顺序预先保存起来,函数还没有执行(柯理化思想)
// 返回一个operate处理函数, 执行operate,并且传递初始值,然后按照之前存储的函数及顺序依次执行
function compose(...funcs){
	return function operate(x){
		//如果没有传递任何函数,则直接将原来的值x返回
		if(funcs.length === 0) return x;
		//如果只传递了一个参数,并且该参数是一个函数,则调用该函数并返回结果
		if(funcs.length === 1) return typeof funcs[0] === 'function' ? funcs[0](x) : x;
		//从右向左遍历
		return funcs.reduceRight(function(result, item, index){
			//如果item不是一个函数,则直接将上一次结果返回
			if(typeof item !== 'function') return  result;
			return item(result);//把得到的结果返回给下一次的result
		}, x);//传递了初始值,则第一次遍历result就是x,item则是第一个函数
	}
}

var add = x => x + 10;
var sub = x => x - 3;
var mul = x => x * 8;
var div = x => x / 2;

let res = compose(div,mul,sub,add)(5)
console.log(res);//48
复制代码

以上就实现了组合函数。

分类:
前端