有关闭包你需要了解的一切
前言
作为一名前端开发人员在学习JavaScript和开发的过程中,对于闭包这个概念一定不会陌生。闭包也是面试过程中考察的重点,弄清楚闭包也是前端开发人员必修技能。
闭包的概念
在《JavaScript 高级程序设计》(第三版)这本书上解释为:
闭包是指有权访问另一个函数作用域中的变量的函数。
为了理解这句话的意思,首先新建一个函数,里面至少放一个变量,于是写了下面这个函数
function showPersonalInfo() {
// 定义一个变量
var name = 'jenemy';
}
然后,在makePersonalInfo
这个函数里面再放一个函数
function showPersonalInfo() {
// 定义一个变量
var name = 'jenemy';
// 定义一个内容函数,接收一个age作为参数
function makeInfo(age) {
return '我的英文名:' + name + ' 年龄:' + age;
}
// 返回个人信息
return makeInfo(25);
}
makePersonalInfo(); // '我的英文名:jenemy 年龄:25'
上面我们在一个函数中新建了另外一个函数,就这样我们创建了一个闭包。
这里需要明确一点:JavaScript允许你引用在当前函数以外定义的任何变量。
在JavaScript中函数名是一个指向函数对象的指针,使用不带圆括号的函数名是访问函数指针,而非调用函数。现在,将上面的示例改成返回一个函数名
function showPersonalInfo() {
var name = 'jenemy';
function makeInfo(age) {
return '我的英文名:' + name + ' 年龄:' + age;
}
// 返回一个函数名
return makeInfo;
}
var show = showPersonalInfo();
show(25); // '我的英文名:jenemy 年龄:25'
然后我们发现:即使外部函数已经返回,但当前函数仍然可以引用在外部函数所定义的变量。
函数可以引用在其作用域内的任何变量,包括参数和外部变量。然后利用这一点我们再次改造上面的函数showPersonalInfo
,给它传递一个参数
function showPersonalInfo(addr) {
var name = 'jenemy';
function makeInfo(age) {
return '我的英文名:' + name + ' 年龄:' + age + ' 住址:' + addr;
}
// 返回一个函数名
return makeInfo;
}
var show = showPersonalInfo('上海市徐汇区');
show(25); // '我的英文名:jenemy 年龄:25 住址:上海市徐汇区'
上面的示例代码明显有些冗余,没有必要中间多一个中间变量makeInfo
,于是稍作调整
function showPersonalInfo(addr) {
var name = 'jenemy';
return function (age) {
return '我的英文名:' + name + ' 年龄:' + age + ' 住址:' + addr;
}
}
上面我们使用了一种更为方便的方式——函数表达式,来构建闭包。注意该表达式是匿名的,我们将其称为匿名函数
现在我们需要更新showPersonalInfo
内部的name
属性的值,于是再对上面的示例代码做一些改变
function showPersonalInfo(addr) {
var name = 'jenemy';
return {
getInfo: function(age) {
return '我的英文名:' + name + ' 年龄:' + age + ' 住址:' + addr;
},
setInfoName: function(newName) {
name = newName;
}
}
}
var person = showPersonalInfo('上海市徐汇区');
person.getInfo(25); // '我的英文名:jenemy 年龄:25 住址:上海市徐汇区'
person.setInfoName('XiaoLu');
person.getInfo(25); // '我的英文名:XiaoLu 年龄:25 住址:上海市徐汇区'
我们将原先返回一个函数名换成了一个对象,并且拥有两个方法getInfo()
和setInfoName()
,然后我们发现当person.setInfoName('XiaoLu')
执行后name
得到了更新。实际上在调用setInfoName()
的时候我们并不知道函数showPersonalInfo()
内部有一个name
变量。使用闭包可以在不公开其外部函数内部变量的同时,允许外部函数利用该内部函数间接使用内部变量
实际上闭包存储的是外部变量的引用,而不是它们的值的副本。这样会导致闭包只能取得包含函数中任何变量的最后一个值
function foo() {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
}
}
return result;
}
var bar = foo();
bar[0](); // 10
接下来我们换一个例子来阐述闭包的其它特殊之处
function demo() {
var n = 0;
return {
add: function(a) {
var innerValue = 1; // 闭包函数局部变量
n += a;
return n;
},
multiply: function(a) {
n *= a;
return n;
}
}
}
foo = test();
bar = test();
foo.add(3); // 3
bar.add(5); // 5
foo.add(7); // 10
bar.multiply(2); // 10
这里有二个函数都引用了变量n
,该例子产生了一个包含了二个闭包的对象。由于闭包引用的是变量的最终值,因此第一次执行foo.add(3)
后,内存中的n
变为了3,然后再继续执行会不断的改变n
的值。
然后需要注意的是闭包函数所引用的外部变量和它的局部变量是不同的。它引用的外部变量是伴随它整个生命周期的,直到它被销毁。但是,它的局部变只有在它被调用的时候被创建,调用结束就会消失。
最后,由于闭包会携带它的函数作用域,所引用的变量会一直存在内容中,这样会导致内存占用过多。所以,在使用闭包的时候还是需要慎重考虑。
闭包的实际运用
下面的一些实例是从网上摘取下来的,可以在实际问题中更好的认识和理解闭包。
这个问题来源自《JavaScript: The Good Parts》,这里我们希望点击每一个li节点都能弹出当前被点击节点的索引。
<ul id="test-ul">
<li>index 1</li>
<li>index 2</li>
<li>index 3</li>
</ul>
<script>
var nodes = document.getElementsByName('li');
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function() {
alert(i); // 4
}
}
</script>
从表面上看,当点击每一个li
标签应该弹出每一个的索引值,实际上每次点击都返回4
。通过一步步断点调试我们会发现在获取到外部函数变量i
的值前实际上外面的循环已经执行完成了,我们最后拿到的是i
的最终值。换句话解释就是:变量i
在我们的闭包中是在函数运行时绑定的,而不是在函数创建的时候。
我们需要做的就是把i
绑定到每一个单独的函数中,不改变外部函数中变量的值。
解决办法一:
for (var i = 0; i < nodes.length; i++) {
(function() {
var j = i;
nodes[i].onclick = function() {
alert(j);
}
})();
}
这里我使用立即调用/执行的函数表达式创建了一个局部作用域,并且将外部函数中的变量i
绑定到了内部函数局部变量j
中,这一步至关重要。
上面的解决方案还可以进一步优化成下面的形式,原因可参考Javascript模块化编程(一):模块的写法
for (var i = 0; i < nodes.length; i++) {
(function(j) {
nodes[i].onclick = function() {
alert(j);
}
})(i);
}
解决办法二:
function helperFunc(i) {
return function() {
alert(i);
}
}
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = helperFunc(i);
}
这里的解决办法是将点击事件处理方法嵌入在另外一个包裹函数中,这样变量i
就处于一个全新的函数作用域中。
解决办法三:
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function(j) {
alert(j);
}.bind(this, i);
}
这里通过bind()
方法直接将变量i
作为参数传入了函数,同时也将作用域绑定到了闭包中。
解决办法四:
[0,1,2,3].forEach(function(i) {
nodes[i].onclick = function() {
alert(i);
}
});
这里使用forEach()
来处理循环虽然也能解决问题,但是循环的次数处理起来并不是很方便。
解决办法五:
for (var i = 0; i < nodes.length; i++) {
var j = i;
nodes[i].onclick = function() {
alert(j);
};
}
这是最简单的处理方法,但是有一个问题,我们给外部函数多增加了一个变量js
,这个完全可以使用立即执行函数表达式封装在独立的作用域中,其实也就演变成解决办法一。
总结
闭包只是一个概念,其实理解起来也不是很难,但是我一开始还是没有去搞清楚,还是花了很多时间去研究这个问题。本文只是起一个抛砖引玉的作用,有阐述不清楚的地方欢迎各们同行多指正。
本文原稿github地址:https://github.com/wujie520303/study-note/blob/master/js学习笔记/有关闭包你需要了解的一切.md