阅读本文大概需要二十分钟
一直有一些刚入门js的朋友问我“什么是闭包?”,这里我就专门总结一下,下次再有人问起来,就直接把这篇文章给他看好了。
为什么闭包这么重要?
这跟js语言的特性有着密切的联系。要想彻底理解闭包,需要首先理解js的作用域概念
(与类C语言的作用域不同!)、立即执行函数
(IIFE)。如果不了解这两个知识点,可以先看文本下半部分对这两个知识点的介绍。
一. 闭包?
其实很简单啊。
闭包就是一个函数(外部函数)内部又定义了一个函数(内部函数),内部函数可以访问外部函数中声明的所有变量。
官方解释就比较晦涩了:
闭包是一个拥有许多变量和绑定了这些变量环境的表达式。
闭包的核心是什么?
由于作用域的关系,我们在函数外部是无法直接访问到函数内部的变量的,闭包就是有权访问另一个函数内部作用域的变量的函数。
如何从内存角度理解闭包?
- JavaScript具有自动垃圾回收机制,函数运行完之后,其内部变量就会被销毁;
- 闭包就是在外部可以访问此函数作用域的变量,所以闭包的一个特点就是只要存在引用函数内部变量的可能,JavaScript就需要在内存中保留这些变量,而且JavaScript运行时需要跟踪这个内部变量的所有外部引用,直到最后一个引用被解除(置为null或者页面关闭),JavaScript垃圾收集器才释放相应的内存空间。
举个例子,
<script type="text/javascript">
function outer(){
var a = 1;
function inner(){
return a++;
}
return inner;
}
var abc = outer();
//outer()只要执行过,就有了引用函数内部变量的可能,然后就会被保存在内存中;
//outer()如果没有执行过,由于作用域的关系,看不到内部作用域,更不会被保存在内存中了;
console.log(abc());//1
console.log(abc());//2
//因为a已经在内存中了,所以再次执行abc()的时候,是在第一次的基础上累加的
var def = outer();
console.log(def());//1
console.log(def());//2
//再次把outer()函数赋给一个新的变量def,相当于绑定了一个新的outer实例;
//console.log(a);//ReferenceError: a is not defined
//console.log(inner);//ReferenceError: a is not defined
//由于作用域的关系我们在外部还是无法直接访问内部作用域的变量名和函数名
abc = null;
//由于闭包占用内存空间,所以要谨慎使用闭包。尽量在使用完闭包后,及时解除引用,释放内存;
</script>
接下来来看一个闭包的经典陷阱——在循环中使用闭包。举个例子,
for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i); //10
}, 0)
}
我们来猜想一下,执行完这个程序输出的是什么?
二. JavaScript的作用域?
作用域控制着变量与参数的可见性与生命周期
,包括三个概念:函数作用域
、块级作用域
、作用域链
。
- 函数作用域
这个很好理解,函数作用域就是说定义在函数中的参数和变量都是函数外部不可见的。这里不做额外介绍,无非就是函数中定义的变量在函数外是无法访问的。
需要注意的是,函数内声明的所有变量在函数体内始终可见的,这也就是变量声明提前
。
var scope="global";
function scopeTest(){
console.log(scope);
var scope="local"
}
scopeTest(); //undefined
此处输出是undefined
,并没有报错,这是因为函数体内声明的变量都在函数体内始终可见。上面代码等效于:
var scope="global";
function scopeTest(){
var scope;
console.log(scope);
scope="local"
}
scopeTest(); //local
- 块级作用域
任何一对花括号中的语句都属于一个块,在这之中定义的变量在括号外无法访问,这叫做块级作用域。
大多数类C语言都有块级作用域的,而JavaScript并不支持块级作用域,它只支持函数作用域,而且一个函数中任何位置定义的变量在该函数中的任何位置都是可见的。
function scopeTest() {
var scope = {};
if (scope instanceof Object) {
var j = 1;
for (var i = 0; i < 10; i++) {
//console.log(i);
}
console.log(i); //输出10
}
console.log(j);//输出1
}
在JavaScript中变量的作用范围是函数级的,所以会在for循环后输出10,在if语句后输出1。
那么在Javascript中如何模拟块级作用域呢?
可以使用IIFE
来模拟一个块级作用域:
(function (){
//内容
})();
举例,
function test(){
(function (){
for(var i=0;i<4;i++){
}
})();
alert(i);
}
test();
函数执行完,弹出的是i
未定义的错误。
- 作用域链
JavaScript中每个函数都有自己的执行上下文环境,当函数调用时会创建变量对象的作用域链,作用域链是一个对象链表,它保证了变量对象的有序访问。
变量的查找会从当前作用域开始找,如果没有就继续向上级作用域链查找,直到找到全局对象中。
三. 立即执行函数?
前面讲到立即执行函数,很多人会把它和闭包混为一谈,这里做一下区分。
它们有着不同的作用,立即执行函数模拟的是块级作用域,防止变量全局污染
,而闭包有不同的作用。
立即执行函数是指声明完便立即执行的函数,这里函数通常是一次性使用的,因此没必要给函数命名,直接让它执行就好了。
所以,立即执行函数的形式应该如下:
<script type="text/javascript">
function (){}(); // SyntaxError: Unexpected token (
//引擎在遇到关键字function时,会默认将其当做是一个函数声明,函数声明必须有一个函数名,所以在执行到第一个左括号时就报语法错误了;
(function(){…})();
//在function前面加!、+、 -、=甚至是逗号等或者把函数用()包起来都可以将函数声明转换成函数表达式;我们一般用()把函数声明包起来或者用 =
</script>
虽然立即执行函数是想在定义完函数后直接就调用,但是引擎在遇到关键字function时,会默认将其当做是一个函数声明,函数声明必须要有一个函数名,所以执行到第一个括号就报错了。
正确地定义一个立即执行函数,是应该用括号把函数声明包起来。
此外,实际应用中,立即执行函数还可用来写插件。
<script type="text/javascript">
var Person = (function(){
var _sayName = function(str){
str = str || 'shane';
return str;
}
var _sayAge = function(age){
age = age || 18;
return age;
}
return {
SayName : _sayName,
SayAge : _sayAge
}
})();
//通过插件提供的API使用插件
console.log(Person.SayName('lucy')); //lucy
console.log(Person.SayName());//shane
console.log(Person.SayAge());//18
</script>
四. 为什么要用闭包?
- 符合函数式编程规范
什么是函数式编程?它的思想是:把运算过程尽量写成一系列嵌套的函数调用。举例来说,要想代码中实现数学表达式:
(1 + 2) * 3 - 4
传统的写法是:
var a = 1 + 2;
var b = a * 3;
var c = b - 4;
函数式编程要求尽量使用函数,把运算过程定义为不用的函数:
var result = subtract(multiply(add(1,2), 3), 4);
此外,函数式编程把函数作为“一等公民”。函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。
- 延长变量生命周期
局部变量本来在函数执行完就被销毁,然而闭包中不是这样,局部变量生命周期被延长。不过这也容易使这些数据无法及时销毁,会占用内存,容易造成内存泄漏。如:
function addHandle() {
var element = document.getElementById('myNode');
element.onclick = function() {
alert(element.id);
}
}
onclick保存了一个element的引用,element将不会被回收。
function addHandle() {
var element = document.getElementById('myNode');
var id = element.id;
element.onclick = function() {
alert(id);
}
element = null;
}
此处将element设为null,即解除对其引用,垃圾回收器将回收其占用内存。
五. tips:
- 如果闭包只有一个参数,这个参数可以省略,可以直接用it访问该参数。
- 实际中闭包常常和立即执行函数结合使用。
六. 参考
http://imweb.io/topic/5665683bd91952db73b41f5e
https://www.cnblogs.com/sspeng/p/6623556.html
http://www.cnblogs.com/dolphinX/archive/2012/09/29/2708763.html
https://segmentfault.com/a/1190000003985390
https://www.jianshu.com/p/0fe03fd2d862
https://segmentfault.com/a/1190000000618597