js闭包与高阶函数

1,769 阅读8分钟

闭包

在js中闭包有两个紧密度非常高的概念与之关联:一.变量的作用域,二.变量的生存周期。那么什么是闭包,我们先看一个闭包的函数: `

function closure () {
    var num = 0;
    return function () {
        if (arguments.length) {
            for (var i = 0, len = arguments.length; i < len; i ++) {
                num += arguments[i]
            }
            return num;
        }
    }
}
var runClosure = closure();
console.log(runClosure(1,3,5,7,9));  //  输出: 25
console.log(runClosure(11,13,15,17,19));    // 输出: 100

`

这是一个很典型且简单闭包函数,closure函数里的匿名函数被返回出来,也就是runClosure,runClosure在外部进行调用,而runClosure所运行的环境是closure这个函数体里的作用域里,而closure这个函数体里的作用域是个封闭的空间,而这样的调用关系就是闭包。简言之:就是一个函数里,包含了另外一个函数,且被外部调用执行,这就形成了闭包。

使用闭包是个很自然的过程,并没有什么特别的,重点是闭包的知识点,文章开头有说到,闭包有两个关联度非常高的概念:变量的作用域与变量的生存周期,先说明,这句话并不是我说的,这句话出自 《javaScript设计模式》一书中,我非常认可这句话,所以照搬了过来。

那么为什么说变量的作用域与变量的生存周期呢,从上面的代码我们可以看出,runClosure是运行在closure这个函数的的局部作用域里,num这个变量也同样生存在这个作用域里面,从我么执行两次runClosure就可以看出,num变量是一直存在的,即便我们在执行一次,它也是在现有结果下进行累加的。我们都知道,在js中存在着全局作用域与局部作用域,全局作用域的变量生存周期是永久的,除非你的页面关闭,而局部作用域里的变量,一般函数体里则是局部作用域,局部作用域里的变量一般跟随调用的函数执行的结束而结束,再次调用就又将是个新的。而闭包里的变量,因为被外包访问到,所以闭包环境里的变量就不能被销毁,便就继续存活着,而这些就是闭包里的知识点,掌握这些知识点,我们就可以利用闭包的特性完成许多奇妙的工作了,比如下面要说的高阶函数。而闭包常见的应用有哪些呢?我们从上面的这个函数里,也可以得出两条结论:

  • 一:封装变量,防止变量被全局变量污染;
  • 二:延长局部变量的生存周期;

高阶函数

什么是高阶函数呢,在《javaScript设计模式》一书中同样有说明:

  • 函数可以作为参数传递;
  • 函数可以作为返回值输出;

满足这些条件之一的都可以称之为高阶函数了,作为参数传递的应用场景就是我们常见的回调函数了:

`

var arr = [13, 25, 10, 17, 8]
arr.sort(function(a, b){
    return a - b;
})

`

像上面数组里sort方法里的这个比较函数就属于高阶函数。而函数作为返回值输出,我们闭包的那个例子里的runClosure就是个高阶函数了。那么为什么会有高阶函数这个概念呢:通俗的讲,高阶函数的应用都是为了解决我们实际开发当中遇到的问题的,高阶函数便因此而诞生的。

那么有哪些常见的高阶函数,它们又解决了我们的什么问题呢?下面我们看几个例子:

  1. 柯里化函数currying。柯里化函数又称部分求值,一个 currying 的函数首先会接受一些参数,接受了这些参数之后, 该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保 存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

`

我们先构建一个需求,假设我们需要对一些数据进行计算总和,我们的目的呢,是得到最后总和得结果,如果我们每一次我们每次把数据输入进去就进行计算,那么明显是浪费计算机资源得,而我们如果把所有得数据先进行存储,在发现后面没有数据了,就把数据计算出总和返回出来,通过这样得操作,我们就优化了性能。下面看代码:

var currying = function (fn) {
			var args = [];
			return function () {
				if (arguments.length) {
					[].push.apply(args, arguments);
					return arguments.callee;    // 意思是返回当前这个匿名函数
				} else { // 如果这个匿名函数参数里没有数字,则进行计算,并返回结果
					return fn.apply(this, args)
				}
			}
		}
		
		var total = (function () {
			var num = 0;
			return function () {
				for (var i = 0, l = arguments.length; i < l; i++) {
					num+=arguments[i];
				}
				return num;
			}
		})()
		
		var cont = currying(total)
		
	cont(1500); cont(3000, 6000); cont(12000); // 这些都未真正计算,只是存储
	console.log(cont()); // 真正计算,并返回结果  输出:22500

`

  1. 函数节流throttle与函数防抖动debounce。函数节流与函数防抖动都有一个共同特点,就是不希望频繁的触发函数运行,比如我们用的onresize事件,onmousemove事件,这些事件都会不经意的被频繁触发,因为频繁的触发函数就要运行函数体,运行函数体就要占用计算资源,还有一些ajax请求也是,如果用户频繁的触发ajax,就会造成不必要的ajax通信,进而占用资源,浪费性能。因此我们就需要封装这样的方法,对于这些方法就是防抖动函数与节流函数了。那么节流与防抖动函数的差别,大家自行百度一下,这里暂不做赘述,我们先看代码实现:

`

// 函数节流
// fn是我们需要包装的事件回调, interval是时间间隔的阈值
	function throttle(fn, interval) {
	// last为上一次触发回调的时间
	let last = 0
	// 将throttle处理结果当作函数返回
	return function () {
	// 保留调用时的this上下文
	let context = this
	// 保留调用时传入的参数
	let args = arguments
	// 记录本次触发回调的时间
	let now = +new Date()		   
	// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
	if (now - last >= interval) {
	// 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
			last = now;
			fn.apply(context, args);
		}
	}
}
	// 用throttle来包装scroll的回调
	const better_scroll = throttle(() => console.log('throttle函数节流'), 1000)	
	document.addEventListener('scroll', better_scroll);
	
// 函数防抖
//  fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
    function debounce(fn, delay) {
    // 定时器
    let timer = null
    // 将debounce处理结果当作函数返回
    return function () {
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 每次事件被触发时,都去清除之前的旧定时器
    if(timer) {
        clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}
// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('debounce函数防抖'), 1000)
document.addEventListener('scroll', better_scroll)

`

  1. 分时函数,分时函数的目的其实很简单,当我们有大量的数据需要进行处理的时候,比如我们要给页面创建1000个div标签,如果一次性添加,就会让浏览器承受不住,会让浏览器显得卡顿。那么我们该怎么办呢,分时函数就是用来应对这些场景的,分时函数的应用有点类似我们的懒加载,懒加载是不满足条件是不触发,分时函数是,无论怎样都要处理完,只是分批次进行的,下面我们以页面添加1000个div为案例演示:

`

    <button id="crearte-btn">开始创建</button>
    // 分时函数
	var  friend = []
	for (let n = 1; n < 1000; n++) {
		friend.push('好友:'+ n + '^_^');
	}
	var timeChunk = function (data, fn, count) {
		var obj, time;
		var len = data.length;
		
		var start = function () {
			for (let i = 0; i < Math.min(count || 1, data.length); i++) { // 小于10或者1
				var obj = data.shift(); // 删除自身一个元素
				fn(obj);
			}
		}
		return function () {
			time = setInterval(function () {	// 每个250毫秒执行一次start方法
				if (!data.length) {
					clearInterval(time)
				}
				start();
			}, 250)
		}
	}
	
	var renderElement = timeChunk(friend, function (n) {
		var div = document.createElement('div');
		div.innerHTML = n;
		document.getElementById('friend-div').appendChild(div);
	}, 10);
	
	document.getElementById('crearte-btn').onclick = function () {
		renderElement();
	}

`

总结

在这个章节里,介绍了闭包与高阶函数,并展示了三种常见的高阶函数的应用,而高阶函数的应用,归根揭底,是用来处理我们日常开发中的一些问题,更多的目的是为了优化性能的操作。今天分享就到这,喜欢的朋友点个赞,谢谢。