这是本系列的第 4 篇文章。前端
做为 JS 初学者,第一次接触闭包的概念是由于写出了相似下面的代码:segmentfault
for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).click = function() { showHelp(item.help); } }
给列表项循环添加事件处理程序。当你点击列表项时不会有任何反应。如何在初学就理解闭包?你须要接着读下去。闭包
说闭包前,你还记得词法做用域吗?函数
var num = 0; function foo() { var num = 1; function bar() { console.log(num); } bar(); } foo(); // 1
执行上面的代码打印出 1。性能
bar 函数是 foo 函数的内部函数,JS 的词法做用域容许内部函数访问外部函数的变量。那咱们可不能够在外部访问内部函数的变量呢?理论上不容许。学习
可是咱们能够经过某种方式实现,即将内部函数返回。spa
function increase() { let count = 0; function add () { count += 1; return count; } return add; } const addOne = increase(); addOne(); // 1 addOne(); // 2 addOne(); // 3
内部函数容许访问其父函数的内部变量,那么将内部函数返回到出来,它依旧引用着其父函数的内部变量。prototype
这里就产生了闭包。code
简单来讲,能够把闭包理解为函数返回函数。对象
上面的代码中,当 increase 函数执行,压入执行栈,执行完毕返回一个 add 函数的引用,因此 increase 函数内部的变量对象依旧保存在内存中,不会被销毁。
调用 addOne 函数,至关于执行内部函数 add,它能够访问其父函数的内部变量,从而修改变量 count。而调用 addOne 函数所在的环境为全局做用域,不是定义 add 函数时的函数做用域。
因此,我理解的闭包是一个函数,它在执行时与其定义时所处的词法做用域不一致,而且具备可以访问定义时词法做用域的能力。MDN 这样定义:闭包是函数和声明该函数的词法环境的组合。
第一,闭包能够在函数外部读取函数内部的变量。
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); Counter.value(); // 0 Counter.increment(); Counter.increment(); Counter.value(); // 2 Counter.decrement(); Counter.value(); / 1
上面这种模式称为模块模式。咱们使用当即执行函数 IIFE 将代码私有化可是提供了可访问的接口,经过公共接口来访问函数私有的函数和变量。
第二,闭包将内部变量始终保存在内存中。
function type(tag) { return function (data) { return Object.prototype.toString.call(data).toLowerCase() === '[object ' + tag + ']'; } } var isNum = type('number'); var isString = type('string'); isNum(1); // true isString('abc'); // true
利用闭包将内部变量(参数)tag 保存在内存中,来封装本身的类型判断函数。
第一,既然闭包会将内部变量一直保存在内存中,若是在程序中大量使用闭包,势必形成内存的泄漏。
$(document).ready(function() { var button = document.getElementById('button-1'); button.onclick = function() { console.log('hello'); return false; }; });
在这个例子中,click 事件处理程序就是一个闭包(在这里是个匿名函数),它将引用着 button 变量;而 button 在这里自己依旧引用着这个匿名函数。从而产生循环引用,形成网页的性能问题,在 IE 中可能会内存泄漏。
解决办法就是手动解除引用。
$(document).ready(function() { var button = document.getElementById('button-1'); button.onclick = function() { console.log('hello'); return false; }; button = null; // 添加这一行代码来手动解除引用 });
第二,若是你将函数做为对象使用,将闭包做为它的方法,应该特别注意不要随意改动函数的私有属性。
如今咱们来解决一下文章开头出现的问题。
function makeHelpCallback(help) { return function() { showHelp(help); }; } for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).click = makeHelpCallback(item.help); }
额外声明一个 makeHelpCallBack 的函数,将循环每次的上下文环境经过闭包保存起来。
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); };
结果为 1 秒后,打印 5 个 5。
咱们能够利用闭包保留词法做用域的特色,来修改代码达到目的。
for (var i = 0; i < 5; i++) { setTimeout((function(i) { return function () { console.log(i); } }(i)), 1000); };
结果为 1 秒后,依次打印 0 1 2 3 4。
闭包在 JS 中随处可见。
闭包是 JS 中的精华部分,理解它须要具有必定的做用域、执行栈的知识。理解它你将收获巨大,你会在 JS 学习的道路上走得更远,好比会在后面的文章来讨论高阶函数和柯里化的问题。
闭包 | MDN
学习 JavaScript 闭包 | 阮一峰
Understanding JavaScript Closures: A practical Approach | Paul Upendo
闭包形成问题泄漏的解决办法 | CSDN
欢迎关注个人公众号 cameraee
前端技术 | 我的成长