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
以上就实现了组合函数。