以下JS高阶编程技巧均属于闭包的高级应用。
文章有点长,请耐心食用
我们来举个例子证明JS中的惰性函数思想。
以单击响应事件为例,
元素.onclick = function(){…}
元素.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(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闭包中的方法。
有两种方法可以解决这个问题:
(function module1{
let index = 0;
function queryData(){};
function getElement(){};
window.getElement = getElement;
})()
(function module2{
let index = 0;
function queryData(){};
window.getElement();
})()
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源码中大量应用了柯里化函数思想
……
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的使用方法:
let arr = [10,20,30,40];
arr.reduce((N,item)=>{
console.log(N,item);
})
=======================
输出结果为:
10 20
undefined 30
undefined 40
了解了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轮循环) 就可了!
终于写完了,谢谢你的陪伴!