闭包的含义和理解
概念:闭包是词法闭包的的简称,是引用了自由变量的函数。这个被引用的自由变量和这个函数一同存在,即使已经离开了创造他的环境也不例外。另一种说法,闭包是由函数和其相关的环境组成的实体
(1)在js中一个函数在另一个函数中定义,可以访问到父函数的成员,内部的函数就称为闭包函数
(2)解释:闭包是词法闭包的的简称,是引用了自由变量的函数
通俗地讲,JavaScript 中每个的函数都是一个闭包,但通常意义上嵌套的函数更能够体现出闭包的特性,请看下面这个例子:
var generateClosure = function() {
var count = 0;
var get = function() {
count ++;
return count;
};
return get;
};
var counter = generateClosure();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3
2
3
4
5
6
7
8
9
10
11
12
这段代码中,generateClosure() 函数中有一个局部变量count,初值为 0。还有一个叫做 get 的函数,get 将其父作用域,也就是 generateClosure() 函数中的 count变量增加 1,并返回 count 的值。generateClosure() 的返回值是 get 函数。在外部我们通过 counter变量调用了 generateClosure() 函数并获取了它的返回值,也就是 get 函数,接下来反复调用几次 counter(),我们发现每次返回的值都递增了1。
让我们看看上面的例子有什么特点,按照通常命令式编程思维的理解,count 是generateClosure 函数内部的变量,它的生命周期就是 generateClosure 被调用的时期,当 generateClosure 从调用栈中返回时,count 变量申请的空间也就被释放。问题是,generateClosure() 调用结束后,counter() 却引用了“已经释放了的” count变量,而且非但没有出错,反而每次调用 counter() 时还修改并返回了 count。这是怎么回事呢?
闭包特点
(1)闭包作为与函数成对的数据,在函数执行过程中属于激活状态, (2)闭包运行结束后,保持运行过程中的最终数据状态 函数闭包的目的:闭包的函数如何访问数据(作用域可见性),以及闭包内的数据如何销毁(数据引用识别)
两种表示
(1)闭包是一个对象,对象里有一个函数,以及被这个函数封闭捕获的变量。 (2)闭包是一种函数,与这个函数相关联的还有捕获变量的环境
闭包的好处
(1)减少全局变量
这正是所谓闭包的特性。当一个函数返回它内部定义的一个函数时,就产生了一个闭包,闭包不但包括被返回的函数,还包括这个函数的定义环境。上面例子中,当函数generateClosure() 的内部函数 get 被一个外部变量 counter 引用时,counter 和generateClosure()的局部变量就是一个闭包。如果还不够清晰,下面这个例子可以帮助你理解:
var generateClosure = function() {
var count = 0;
var get = function() {
count ++;
return count;
};
return get;
};
var counter1 = generateClosure();
var counter2 = generateClosure();
console.log(counter1()); // 输出 1
console.log(counter2()); // 输出 1
console.log(counter1()); // 输出 2
console.log(counter1()); // 输出 3
console.log(counter2()); // 输出 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面这个例子解释了闭包是如何产生的: counter1和counter2分别调用了generate-Closure() 函数,生成了两个闭包的实例,它们内部引用的 count 变量分别属于各自的运行环境。我们可以理解为,在 generateClosure() 返回 get 函数时,私下将 get 可能引用到的 generateClosure() 函数的内部变量(也就是 count 变量)也返回了,并在内存中生成了一个副本,之后 generateClosure() 返回的函数的两个实例 counter1和 counter2 就是相互独立的了。
闭包的用途
1. 嵌套的回调函数
闭包有两个主要用途,一是实现嵌套的回调函数,二是隐藏对象的细节。让我们先看下面这段代码示例,了解嵌套的回调函数。如下代码是在 Node.js 中使用 MongoDB 实现一个简单的增加用户的功能(可以说每一个nodejs的高手,通常也是js的高手):
exports.add_user = function(user_info, callback) {
var uid = parseInt(user_info['uid']);
mongodb.open(function(err, db) {
if (err) {callback(err); return;}
db.collection('users', function(err, collection) {
if (err) {callback(err); return;}
collection.ensureIndex("uid", function(err) {
if (err) {callback(err); return;}
collection.ensureIndex("username", function(err) {
if (err) {callback(err); return;}
collection.findOne({uid: uid}, function(err) {
if (err) {callback(err); return;}
if (doc) {
callback('occupied');
} else {
var user = {
uid: uid,
user: user_info,
};
collection.insert(user, function(err) {
callback(err);
})
}
})
})
})
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
如果你对 Node.js 或 MongoDB 不熟悉,没关系,不需要去理解细节,只要看清楚大概的逻辑即可。这段代码中用到了闭包的层层嵌套,每一层的嵌套都是一个回调函数。回调函数不会立即执行,而是等待相应请求处理完后由请求的函数回调。我们可以看到,在嵌套的每一层中都有对 callback 的引用,而且最里层还用到了外层定义的 uid 变量。由于闭包机制的存在,即使外层函数已经执行完毕,其作用域内申请的变量也不会释放,因为里层的函数还有可能引用到这些变量,这样就完美地实现了嵌套的异步回调。
2. 实现私有成员
我们知道,JavaScript 的对象没有私有属性,也就是说对象的每一个属性都是曝露给外部的。这样可能会有安全隐患,譬如对象的使用者直接修改了某个属性,导致对象内部数据的一致性受到破坏等。JavaScript通过约定在所有私有属性前加上下划线(例如_myPrivateProp),表示这个属性是私有的,外部对象不应该直接读写它。但这只是个非正式的约定,假设对象的使用者不这么做,有没有更严格的机制呢?答案是有的,通过闭包可以实现。让我们再看看前面那个例子:
var generateClosure = function() {
var count = 0;
var get = function() {
count ++;
return count;
};
return get;
};
var counter = generateClosure();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3
2
3
4
5
6
7
8
9
10
11
12
13
我们可以看到,只有调用 counter() 才能访问到闭包内的 count 变量,并按照规则对其增加1,除此之外决无可能用其他方式找到 count 变量。受到这个简单例子的启发,我们可以把一个对象用闭包封装起来,只返回一个“访问器”的对象,即可实现对细节隐藏
闭包的注意点
(1)如果闭包捕获父环境中的变量,只是引用,不是复制 (2)父函数每次调用会产生不同的闭包 (3)循环的时候会出现的问题
JAVASCRIPT中的标识符查找遵循从内到外的原则。
function foo() {
var local = 'Hello';
return function() {
return local;
};
}
var bar = foo();
console.log(bar()); //=> Hello
/**这里所展示的让外层作用域访问内层作用域的技术便是闭包(Closure)。得益于高阶函数的应用,使foo()函数的作用域得到`延伸`。foo()函数返回了一个匿名函数,该函数存在于foo()函数的作用域内,所以可以访问到foo()函数作用域内的local变量,并保存其引用。而因这个函数直接返回了local变量,所以在外层作用域中便可直接执行bar()函数以获得local变量。**/
2
3
4
5
6
7
8
9
10
闭包是JAVASCRIPT的高级特性,因为把带有内部变量引用的函数带出了函数外部,所以该作用域内的变量在函数执行完毕后的并不一定会被销毁,直到内部变量的引用被全部解除。所以闭包的应用很容易造成内存无法释放的情况。
良好的闭包管理。
循环事件绑定、私有属性、含参回调等一定要使用闭包时,并谨慎对待其中的细节。
循环绑定事件,我们假设一个场景:有六个按钮,分别对应六种事件,当用户点击按钮时,在指定的地方输出相应的事件。
var btns = document.querySelectorAll('.btn'); // 6 elements
var output = document.querySelector('#output');
var events = [1, 2, 3, 4, 5, 6];
// Case 1
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function(evt) {
output.innerText += 'Clicked ' + events[i];
};
}
/**这里第一个解决方案显然是典型的循环绑定事件错误,这里不细说,详细可以参照我给一个网友的回答;而第二和第三个方案的区别就在于闭包传入的参数。**/
// Case 2
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function(index) {
return function(evt) {
output.innerText += 'Clicked ' + events[index];
};
})(i);
}
/**第二个方案传入的参数是当前循环下标,而后者是直接传入相应的事件对象。事实上,后者更适合在大量数据应用的时候,因为在JavaScript的函数式编程中,函数调用时传入的参数是基本类型对象,那么在函数体内得到的形参会是一个复制值,这样这个值就被当作一个局部变量定义在函数体的作用域内,在完成事件绑定之后就可以对events变量进行手工解除引用,以减轻外层作用域中的内存占用了。而且当某个元素被删除时,相应的事件监听函数、事件对象、闭包函数也随之被销毁回收。**/
// Case 3
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function(event) {
return function(evt) {
output.innerText += 'Clicked ' + event;
};
})(events[i]);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
避开闭包陷阱
闭包是个强大的工具,但同时也是性能问题的主要诱因之一。不合理的使用闭包会导致内存泄漏。
闭包的性能不如使用内部方法,更不如重用外部方法。
由于IE 9浏览器的DOM节点作为COM对象来实现,COM的内存管理是通过引用计数的方式,引用计数有个难题就是循环引用,一旦DOM引用了闭包(例如event handler),闭包的上层元素又引用了这个DOM,就会造成循环引用从而导致内存泄漏。
善用函数
使用一个匿名函数在代码的最外层进行包裹。
;(function() { // 主业务代码 })();
有的甚至更高级一点:
;(function(win, doc, $, undefined) {
// 主业务代码
})(window, document, jQuery);
2
3
甚至连如RequireJS, SeaJS, OzJS 等前端模块化加载解决方案,都是采用类似的形式:
/**RequireJS**/
define(['jquery'], function($) {
// 主业务代码
});
/**SeaJS**/
define('module', ['dep', 'underscore'], function($, _) {
// 主业务代码
});
2
3
4
5
6
7
8