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);
}
}