1 闭包定义
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
// 示例1 function makeFunc() { // name是个局部变量,在makeFunc的函数作用域中 var name = "Mozilla"; function displayName() { // displayName函数使用了自己函数作用域以外的变量 alert(name); } return displayName; } var myFunc = makeFunc(); myFunc(); // 示例2 function makeAdder(x) { // x是makeAdder函数的入参,在makeAdder的函数作用域中,被一个匿名函数使用了 return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
2 使用场景
2.1 回调函数
// 示例1 // 在页面上添加一些可以调整字号的按钮。 function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16); document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
2.2 封装私有变量、私有函数
var makeCounter = function() { // 私有变量 var privateCounter = 0; // 私有函数 function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; /* makeCounter执行一次会生成新的局部变量privateCounter,由于有闭包,makeCounter执行完后 * privateCounter不会释放,每个闭包都是引用自己词法作用域内的变量privateCounter */ var Counter1 = makeCounter(); var Counter2 = makeCounter(); console.log(Counter1.value()); /* logs 0 */ Counter1.increment(); Counter1.increment(); console.log(Counter1.value()); /* logs 2 */ Counter1.decrement(); console.log(Counter1.value()); /* logs 1 */ console.log(Counter2.value()); /* logs 0 */
2.3 防抖与节流(闭包的典型应用)
// 防抖 function debounce (fn, delay=500) { let timer = null; return function() { if(timer) { clearTimeout(timer); } timer = setTimerout(() => { fn.apply(this, arguments); timer = null; }, delay); } } // 节流 function throttle (fn, delay=500) { let timer = null; return function() { if(timer) { return; } timer = setTimerout(() => { fn.apply(this, arguments); timer = null; }, delay); } }
3 与闭包经常一起使用的语法 IIFE(立即调用函数表达式)
3.1 IIFE定义
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
写法: (函数声明)(函数参数)
3.2 IIFE特性
当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。
(function () { var name = "Barry"; })(); // 无法从外部访问变量 name name // 抛出错误:"Uncaught ReferenceError: name is not defined"
将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果
var result = (function () { var name = "Barry"; return name; })(); // IIFE 执行后返回的结果: result; // "Barry"
4 使用闭包时的常见错误
4.1循环中创建闭包
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { // var变量只有全局作用域及函数作用域,这里每次循环使用的是同一个item var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } }
赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。三次循环后,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。
解决办法:
1.使用更多闭包
// 其余地方不变,这里只修改了循环部分 for (var i = 0; i < helpText.length; i++) { // 三次循环生成了三个匿名函数,每个onfocus的回调绑定了一个各自的词法作用域 (function() { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } })(); // 马上把当前循环项的item与事件回调相关联起来
2.使用es5中的let
// 其余地方不变,这里只修改了循环部分 for (var i = 0; i < helpText.length; i++) { // let具有块级作用域,循环三次生成了三个 let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } }
5 性能考量
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。应该尽量少使用闭包。
// 这个构造函数每次调用MyObject都会生成新的getName、getMessage方法 function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; } // 推荐改为 function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
参考
[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures) [MDN](https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE)