JavaScript -- 闭包


作者: DocWhite白先生

一. 对闭包的误解

我在面试一些刚入行的新人的时候,对于JavaScript基础部分一定会问什么是闭包,闭包有什么作用,有什么弊端?大部分人只能回答什么是闭包,闭包的概念,以及闭包会产生内存泄露,但是大部分都没有具体的利用闭包解决实际问题的经验,就无法解答闭包有什么作用。同时由于实际操作次数不多,以及口口相传的闭包会参数内存泄露问题,导致大多数人对闭包误解很深,以至不愿意使用闭包。

而实际上,虽然闭包有其易导致内存泄露的问题,但是只要有手动清楚闭包引用变量的习惯,完全可以人为的避免掉闭包带来的缺陷。

二. 一个简单的闭包

function autoCount() {
	const count = 0;
	return function() {
		console.log(count);
		return count += 1;
	}
}

var count = autoCount();
count();			// terminal print:  0
count();			// terminal print:  1
...
count();			// terminal print:  n

这是一个最简单的关于闭包的例子。这个例子告诉我们什么? 闭包能够延长局部变量的寿命!
由于JavaScript垃圾回收机制的实现,是基于是否还存在对于该变量的引用而决定是否回收该变量。上例中autoCount方法执行后返回的是一个新的匿名函数,但是该匿名函数的存在依赖着函数外部变量count,这个count变量存在于autoCount的函数体内,正常情况(如果返回的结果不是依赖于count的函数)下autoCount函数执行完毕后,它的局部变量count会被垃圾回收机制收回并释放被占用的内存。
但是此时autoCount执行完后返回的是一个未被执行的匿名函数,而该函数对于count变量有强依赖,导致count无法通过垃圾回收机制回收,因为它的引用依然存在。这样就使得局部变量count常驻内存中无法释放。

不知道有没有人注意到另外一点,闭包除了能延长局部变量的寿命,同时还能起到惰性求值的作用。注意autoCount函数的执行返回的结果是另外一个函数,但是这个函数在autoCount函数执行完毕后并没有马上执行,利用这个特点,就可以做到惰性求值。

三. 利用闭包实现惰性求值

什么是惰性求值?就是结果并不会马上进行结果计算,只有达到特定条件才会真正触发结果的计算。这有什么用?
例如我们需要统计一个月的生活开销。仅仅需要在月底查看总开销,并不需要每天记录了每日的消费后马上计算月的总开销。
这时候利用闭包和函柯里化的概念(function currying),就能实现这种惰性求值。
下面是一个简单的例子:

const currying = function(fn) {
	var args = [];
	return function() {
		if(arguments.length === 0){
			return fn.apply(this, args);
		}else{
			[].push.apply(args, arguments);
			return arguments.callee;
		}
	}
};

let cost = (function() {
	let money = 0;
	return function() {
		for(let i = 0, l < arguments.length; i < l ; i += 1){
			money += arguments[i];
		}
		return money;
	}
})();

cost = currying( cost );
cost(300);	// 未求值
cost(200);	// 未求值
cost(300); 	// 未求值
cost();     // 800;

当然,闭包能做到的还不仅仅是延长变量寿命,惰性求值,还可以实现AOP。

四. AOP

AOP(面向切面编程)的主要作用是把一些与核心业务无关的模块抽离出去,这些模块抽离出来之后,再通过动态织入的方式掺入业务逻辑模块中,这样做的好处是保持业务逻辑模块的纯净和高内聚性。可以这些概念并不容易理解,举个简单例子。

function Main(type) {
	console.log("Main Job!");
}

Main.prototype.before = function(beforeFn) {
	const __self = this;
	return function () {
		beforeFn.apply(this, arguments);
		return __self.apply(this, arguments);
	}
}

Main.prototype.after = function(afterFn) {
	const __self = this;
	return function() {
		__self.apply(this, arguments);
		afterFn.apply(this, arguments);
	}
}

const job = Main.before(() => {console.log('Do job before main!')})
 			    .after(() => {console.log('Do job after main!')});
job();   //  terminal print:  Do job before main!
		 //  terminal print:  Main Job!
		 //  terminal print:  Do job after main!

五. 函数节流

闭包除了能实现上述功能,还能实现函数节流(throttle),什么是函数节流,举个简单例子:
当我们为window添加onresize事件监听时,当我们一旦拖拽改变浏览器宽高,这个onresize事件的触发频率是非常高的,会对浏览器性能产生一定的影响,如果这时候能人为的为onresize事件设定触发条件,限制其触发频率,就显得尤为重要,尤其是当onresiz事件中出现大量的dom操作时。
下面是一个简单的throttle实现:

function throttle(fn, interval) {
	var __self = fn, timer, isFirst = true;
	return function() {
		var args = arguments, __me = this;
		if(isFirst){
			__self.apply(__me, args);
			return isFirst = false;
		}
		if(timer){
			return  false;
		}
		timer = setTimeout(function() {
			clearTimeout(timer);
			timer = null;
			__self.apply(__me, args);
		}, interval || 500)
	}
}

window.onresize = throttle(function() {
	console.log("I only trigger a time per second! ");
},  1000)

六. 分时函数

与函数节流不同,有些情况下,函数是被用户主动调用,并且这些函数可能会严重的影响页面性能。
举个例子,在某个页面中,用户通过点击按钮加载成百上千条表格数据,如果一条数据代表一个Dom节点,这意味着需要一次性往页面中创建成百个节点。这会菱浏览器吃不消甚至卡死。这时候我们就需要一个分时函数批量处理Dom更新,而非一次性更新。

function timeChunk(arr, fn, count) {
	var obj, t;
	var len = arr.length;
	function start() {
		for(var i = 0; i < Math.min(count || 1, arr.length ); i += 1){
			var obj =  arr.shift();
			fn(obj);
		}
	}
	return function() {
		t = setInterval(function() {
			if(arr.length === 0){
					return clearInteval(t);
			}
			start();
		}, 200);
	}
}
  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值