JS高阶编程技巧——惰性函数&单例设计模式、柯里化函数思想、compose函数

以下JS高阶编程技巧均属于闭包的高级应用。
文章有点长,请耐心食用

目录

  • 惰性函数思想
  • 单例设计模式
  • 柯里化函数思想
  • compose函数——实现函数调用扁平化
    • 数组的reduce方法

惰性函数思想

我们来举个例子证明JS中的惰性函数思想。

以单击响应事件为例,

  • DOM 0 级事件绑定方式为: 元素.onclick = function(){…}
  • DOM 2 级事件绑定方式(大多数)为: 元素.addEventListener("click",function(){…}),但这种方式不被IE6~8所支持,在低版本浏览器中采用这种方式: 元素.attachEvent("onclick",function(){…})

我们希望基于DOM2进行事件绑定,并兼容所有浏览器。
自然而然需要进行判断:

/* observerEvent:基于DOM2进行事件绑定
* @params:
*		element: 需要绑定事件的元素
*		type: 绑定的事件类型
*		func: 事件响应函数
*	@return: undefined
*/
function observerEvent(element,type,func){
	if(element.addEventListener){ 
		element.addEventListener(type, func);     // 如果有这个就用这个
	}else if(element.attachEvent){
		element.attachEvent("on" + type, func);  // 不行的话有这个用它也可以
	}else{
		element["on" + type] = func;     // 再不行就得用DOM0事件绑定方法
	}
}

这个方法可以实现功能,但如果多次执行,每一次都要判断浏览器的兼容性,是在有点麻烦。我们希望在第一次执行方法的时候做一次判断,以后不要重复判断了。

function observerEvent(element,type,func){
	if(element.addEventListener){ 
		observerEvent = function(element,type,func){
			element.addEventListener(type, func); 
		}
	}else if(element.attachEvent){
		observerEvent = function(element,type,func){
			element.attachEvent("on" + type, func);
		}
	}else{
		observerEvent = function(element,type,func){
			element["on" + type] = func; 
		}
	}
	observerEvent(element,type,func);
}

以上代码主要做了两点修改:

  • 函数第一次执行,无论进入哪一个判断,大函数形成的私有上下文都会产生一个新的函数堆,并且将这个堆的地址赋给全局的observerEvent变量,形成不被销毁的上下文,从而形成闭包。observerEvent函数完成了重构,新指向的这个函数是不需要做兼容判断的。
  • 添加了observerEvent(element,type,func)这句代码。因为在函数第一次执行时,函数重构只是新建了函数堆、更改了函数指向,并没有执行函数,因此需要加一句代码,手动执行重构之后的方法,实现事件绑定。

单例设计模式

单例设计模式是最早的模块化开发思想的体现。(框架开发下就不需要了)
比如,2人合作开发一个项目,A负责module1的功能开发,B负责module2的功能开发。两人为了防止变量冲突,会将自己写的代码放在一个闭包里,闭包内所有的变量都是私有的:

(function module1{
	let index = 0;
	function queryData(){};
})()

(function module2{
	let index = 0;
	function queryData(){};
})()

One day, A封装了一个非常好用的方法getElement,B希望"copy"过来供自己所用 ( 这里我们不讨论ctrl+c/ctrl+v的这种简单粗暴的方法emmm ),但是正常情况下,B无法调用A闭包中的方法。
有两种方法可以解决这个问题:

  • B对A说,你把我想借鉴的那个方法暴露到全局上吧,这样我也能用啦。
    不得不说,如果想向全局暴露的方法非常多,也会引起变量冲突,与直接写在全局没啥区别。
(function module1{
	let index = 0;
	function queryData(){};
	function getElement(){};
	window.getElement = getElement;
})()

(function module2{
	let index = 0;
	function queryData(){};
	window.getElement();
})()
  • 通过return一个对象的方式,把一些方法暴露出去。
let moduleA = (function module1{
	let index = 0;
	function queryData(){};
	function getElement(){};
	return{
		getElement       // 是getElement: getElement的简写(属性名和属性值相同可简写)
	}
})()

let moduleB = (function module2{
	let index = 0;
	function queryData(){};
	moduleA.getElement();     // 模块2通过这种方式调用模块1中暴露出来的方法
})()

通过这种方式,我们暴露在全局的只有moduleA和moduleB这样的命名空间名。将描述相同事务(相同版块)中的属性&方法归拢到相同的命名空间下,实现分组管理,既不会污染全局变量空间、引起变量冲突,又可以将一些自己私有的方法暴露出来,以便其他模块调用。

除此之外,我们还会在return的对象中添加一个init属性,该属性值是一个函数。在单例模式设计的基础上,再加一个命令模式。init作为当前模块业务的入口:

let moduleA = (function module1{
	let index = 0;
	function queryData(){};
	function getElement(){};
	function bindHTML(){};
	function handleEvent(){};
	return{
		init(){
			getElement();
			bindHTML();
			handleEvent();
		}
	}
})()
moduleA.init();

调用这个模块方法的时候,只需要执行init方法就可以了,我们在init方法中,根据业务需求,把编写的方法按照顺序依次调用执行即可。

柯里化函数思想

介绍:这是一个应用很普遍的思想~
它的核心概念——“预先存储&处理”;
它的常见形式——大函数传入一些参数,返回一个小函数对传进来的参数进行处理。

拿一道题来解释一下:

写一个函数fn实现如下功能:
let res = fn(1,2)(3);
console.log(res);    => 6(1+2+3)

首先分析一波:函数fn(1,2)执行之后还可以继续执行并传入参数3,说明函数fn执行返回了一个函数,并且这个函数还有一个形参。
我们暂且认为参数是固定个数的:

function fn(x,y){
	return function(z){
		return x+y+z;
	}
}
let res = fn(1,2)(3);
console.log(res); 

可以看出,第一次执行函数时,fn形成的私有上下文创建的一个函数堆被全局变量res所引用,因此这个私有上下文不能被销毁,它的私有变量x、y于是得以保存下来,在执行小函数时,沿着作用域链找到上级上下文中的x、y,并进行后续处理。这就是柯里化函数思想的具体体现。

其他应用:
Function.prototype.bind中预先处理this
redux、react-redux源码中大量应用了柯里化函数思想
……

compose函数——实现函数调用扁平化

const add1 = (x) => x+1;
const mul3 = (x) => x*3;
const div2 = (x) => x/2;
let result = div2(mul3(add1(add1(0))));
console.log(result);

通过函数的嵌套调用,我们得到最后结果为3。但嵌套的函数数目较多时代码可读性非常差,于是我们想要一个这样的函数compose(div2,mul3,add1,add1)(0)实现同样的功能,实现函数调用的扁平化。

场景:一个函数的执行结果作为实参,传递给下一个函数执行。
前提:这些函数只接收一个参数。
f(g(h(x)))------>compose(f,g,h)(x)/compose(h,g,f)(x),函数参数的顺序可以自己指定。
那compose函数该怎么写呢?

首先我们了解一下数组迭代方法reduce的使用方法:

数组的reduce方法

let arr = [10,20,30,40];
arr.reduce((N,item)=>{
	console.log(N,item);
})
=======================
输出结果为:
10 20
undefined 30
undefined 40
  • reduce方法可传两个参数:
    • 第一个参数为函数(必传):即数组迭代到每一项时执行的回调函数,函数有两个形参N,item。其中item为数组迭代时的当前项。
    • 第二个参数为基本类型值(选传)
  • 只传一个参数时(上例),N的初始值为数组arr第一项,item从第二项开始遍历;之后N的值为上一次函数返回的结果(return的值)。由于上例中函数没有返回值,因此N的值为undefined。
  • 传两个参数时,N的初始值为第二个参数值,item则从第一项开始遍历;之后N的值为上一次函数返回的结果(return的值)。
  • 总结:
    • N为上一次函数返回的结果,但其初始值取决于是否传第二个参数。
    • item为数组迭代时的当前项,迭代开始的起点取决于是否传第二个参数。
    • 原数组不变,结果以返回值的形式返回。

了解了reduce方法之后,我们再看compose函数该咋写,需要分类讨论:

function compose(...funcs){
	return function anonymous(val){        // 既然可以连续执行,返回值就是一个函数
		if(funcs.length === 0) return val;   // 如果函数形参列表为空,就返回传入的值
		if(funcs.length === 1) return funcs[0](val);    // 如果只有一个函数,执行就可
		return funcs.reverse().reduce((N,item)=>{          // 重点来了!解析在下面
			return typeof N==="function" ? item(N(val)) : item(N);
		})
	}
}

当函数列表中有两个以上的函数执行时,需要用到reduce方法遍历funcs这个函数参数数组。

  • funcs数组是否需要反转reverse,取决于将数组扁平化之后函数参数的执行顺序,在这个例子中,compose(div2,mul3,add1,add1)(0)这个函数的执行顺序是从后向前的,因此需要进行反转。

  • 上面代码中,reduce没有传第二个参数,我们来捋一下每轮迭代的结果:

    N item 操作
    add1 add1 add1(add1(0)) => 下一轮N item(N(val))
    N mul3 mul3(add1(add1(0))) => 下一轮N item(N)
    N div2 div2(mul3(add1(add1(0)))) => 下一轮N item(N)
    N item(N)

    于是诞生了这段代码:

    return funcs.reverse().reduce((N,item)=>{        
    	return typeof N==="function" ? item(N(val)) : item(N);
    })
    

    实际还有更简单的写法:

    return funcs.reverse().reduce((N,item)=>{  
    	return item(N)
    },val)
    

    由于传了第二个参数val,因此N的初始值为val,只需每轮迭代执行item(N),然后将结果赋值给N (此处省略xxxxxx轮循环) 就可了!

终于写完了,谢谢你的陪伴!

你可能感兴趣的:(JavaScript)