简单介绍几个js高阶函数实现的功能,例子来自于此书 JavaScript设计模式与开发实践 前期的几个对高阶函数的介绍,用处还是很大的,特别是柯里化与AOP,在日常开发中经常会用到
学习前,你必须先掌握的知识
首先,apply我就不讲了,我之前已经介绍过了,而且我觉得稍微研究过js的应该都会,闭包的话我还是举个例子说明一下吧;
简单举一个记录调用次数的闭包来说明
function closure () {
var time = 0;
return function () {
time++;
return time
}
}
var func = closure()
func() // 1
func() // 2
func() // 3
这里执行函数func得到的time分别是1、2、3,每执行一次time在闭包内部就自增1,那么为什么这里closure内的局部变量time没被回收呢?因为js认为你的内部函数在执行的过程中调用了外部closure作用域中的time变量,所以认为外部作用域仍然对你有用,所以就没有进行自动回收的处理,所以很明显闭包其实就是一个块级作用域的原理,在没有ES6的let关键词之前,我们都是这么实现块级作用域中变量的保存的,而这里被外部函数保存且没被js编译器回收的变量就是time,好了闭包就简单介绍到这了。
接下来,介绍第一个高阶函数的运用
AOP是java的spring框架中的一个重要思想,也是函数式编程的一个分支,主要用于在不破坏代码原有逻辑的情况下对函数进行扩展操作,如日志生成及函数监控,最明显的例子是有时候在某个函数执行前我们需要调用一个lock锁来锁住这个函数不让他继续被调用,当函数执行完毕后,我们再来调用一个函数来解锁,而锁中当然可以进行你所有想要实现的事。
因为加锁和解锁都是函数封装实现嘛,下面的demo就是js的实现方法。
// AOP
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments)
_self.apply(this, arguments)
}
}
Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments)
console.log(ret)
return ret
}
};
var func = function (num) {
console.log(num)
};
func = func.before(function () {
console.log(1)
}).after(function () {
console.log(3)
});
func(2)
输出的结果依次是1、2、3,我们可以在demo中看到实现原理其实非常简单,就是在Function的原型中注入2个函数来实现,而before函数的作用就是利用闭包,重构函数的执行顺序,先让注入的依赖函数beforeFn执行,然后在让自身主函数执行,而after的原理也是一样的,有同学可能会说只是改变函数执行顺序不用闭包也可以,确实是这样,但是闭包在这里是有必要使用的,并不仅仅只是为了单纯改变函数调用顺序而已
柯里化,完美运用闭包保存变量的功能实现函数的节流。
下面给大家举两个例子来体会柯里化的优点
一、计费例子,我们有时可能是多段计费,但真正需要的只是最后的总数,并不需要每段的得数。
先来个柯里化
var costSimply = (function () {
var args = []; // 记录每段传入的钱
return function () {
if (arguments.length !== 0) {
Array.prototype.push.apply(args, arguments);
} else {
var count = 0;
args.forEach(function (val) {
count += val;
});
return count
}
}
})()
costSimply(100, 200, 300) // 第一段我们分别花了 100 200 400
costSimply(400) // 第二段我们花了 400
costSimply(500, 500) // 第三段我们分别花了 500 500
var count = costSimply() // 合计输出
console.log(count) // 2000
你想什么时候计算总和返回就不传参数即可直接计算,一直输入参数一直保存在args中并不做计算,原理就是js闭包的优点,但是明显这不够函数式编程,也不是函数柯里化,所以我们下面把计费的过程也封装起来柯里化就可以了 ↓
var cost = function () {
var count = 0;
Array.prototype.forEach.call(arguments, function (val) {
count += val
})
return count
}
var currying = function (fn) {
if (typeof fn === 'function') {
var args = [];
return function () {
if (arguments.length !== 0) {
Array.prototype.push.apply(args, arguments)
} else {
return fn.apply(this, args)
}
}
} else {
console.error(`function ${arguments.callee.name}’s args must be function`)
}
}
cost = currying(cost); // 函数柯里化
cost(100, 200 ,300)
cost(400)
cost(500, 500)
cost() // 2000
字符串我就不用ES5来拼了,太麻烦了,其他还是用ES5来写给大家看。
二、函数返回值的存储,也是利用闭包特性的一个存储方式,此例子原理与将大批量的数组数据,进行匹配后,利用哈希数组存储起来,再次访问的时候直接访问键名直接索引的道理是一致的,但是代码特别精巧,来自于vue源码的 函数返回值 缓存(单例模式的多重实现)
function log(string) {
return string
}
var cached = function (fn) {
const cache = Object.create(null);
return function (string) {
const result = cache[string]
return result || (cache[string] = fn(string))
}
}
cached = cached(log)
cached(10)
cached(10)
cached(10)
大家可以发现,其实函数是只执行了一次的,当再次调用函数时,实际是通过哈希数组去索引得来的,免去函数中各类数据结构繁琐的操作,当函数参数为同一参数时,我们直接会return cache[string]
,直接由键名直接寻址,根本没有函数操作,此时特别适合大量数据的重复引用与复杂的数据结构处理,当然这只是vue源码中一个极小的地方。
接下来仍然是书中介绍的一个高阶函数的实现,当然,也是利用闭包
var throttle = function (fn, interval) {
var _self = fn,
first = true,
timer;
return function () {
if (first) {
_self.bind(this, (new Date()).getSeconds()).apply(this, arguments)
return first = false
}
if (timer) {
return 0
}
timer = setTimeout(function () {
_self.bind(this, (new Date()).getSeconds()).apply(this, arguments);
clearTimeout(timer)
timer = null
}.bind(this),interval || 500)
}
}
window.onresize = throttle(function () {
console.log(arguments[0])
},1000)
为了方便大家看效果,我特地bind了执行函数时的当前秒数上去,写出来后大家明显看到log出来的时间间隔是 1s ,原理也非常简单,就是创建一个一次性定时器,让他等候在执行队列中,如果上一次的定时器仍然在执行,那么变量锁就仍然存在,再次调用时不会执行,当按照间隔时间执行完毕后,锁即会打开,就是限制为空了,唯一需要处理的就是第一次无等候队列的执行了。
与图片懒加载与函数节流有异曲同工之妙,只不过懒加载增加了每段图片加载完成的回调而已,思想都是分段执行。
var arr = [];
for (var i = 0; i <= 100; i++) {
arr.push(i);
}
var timeChunk = function (array, fn, count, interval) {
var timer,
data = array;
var start = function () {
for (var i = 0, l = data.length; i < Math.min(l, count || 1); i++) {
var obj = data.shift();
fn(obj)
}
}
return function () {
start()
timer = setInterval(function () {
if (data.length === 0) {
timer = null;
clearInterval(timer);
}
start()
}, interval || 500)
}
}
var renderNumber = timeChunk(arr, function (n) {
var div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div)
}, 10, 1000)
renderNumber()
大家可以看出原理跟函数节流非常像,函数节流是利用一次性定时器控制频繁触发的函数时间,将函数队列加了一层锁的概念,而分时函数则是通过周期性定时器,动态分段渲染以往一次性渲染的大量数据,都含有节流的概念,而分时函数对闭包的应用与节流一样,利用外部data数据的长度来判断是否将定时器回收,而控制长度的精髓主要在于i < Math.min(l, count || 1)
这段限制条件,取数据长度与需要渲染数量的最小值,以防数据长度因不停的出栈而导致最终剩余的数组长度data.length
小于约定的渲染数量count
,而后面count || 1
也是为了防止不传入count时的默认渲染个数,除了注重对闭包的应用外,我们在这两个例子中,我认为更重要的是学习代码书写的严谨性以及利用定时器来完成分时与节流这种控制函数执行的思想。
例子就在这里全部举完了,主要都是书上与自己平常看到的一些好代码,本来有些例子想自己举出,但想到vue的一些源码和多看几遍书中例子后,感觉个人举出来的例子远没有上述典型、优秀,所以都是反复写几遍自己吸收后的一些理解与记录,方便自己以后回来复习,同时也希望通过不停的学习以后能够写出这样思想优秀谨慎的代码吧。
题外话:最近java同事(这里就简称为涛哥)帮我写了一个python脚本用于webpack的自动build,使我处的项目组直接svn上传代码后便自动执行npm run build
打包,并将打包后的js、css、index直接添加到项目目录下,免去了之前上传代码后还要手动打包丢到测试环境的步骤,虽然原理十分简单,就是每隔一段时间就监控svn代码版本号的变化,若变化,则在服务器执行打包命令,并完成后将指定文件复制至指定目录下。
但是最关键的是我看了下python的代码,惊了!竟然如此简单,我从来没学过都完全看懂了,跟C简直基本上一模一样,从前一直听说python大法好,这次直接性的感受到了之后,我只想说,python大法简直狂拽炫酷叼炸天好吗。Life is short ,use python!有空绝对学一波!!!