一、什么是闭包
MDN中对闭包有以下定义:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
从上述定义中我们可以总结出4点(重点
):
- 1、
闭包
是在函数创建
时创建的,即有函数创建就会生成闭包
; - 2、
闭包
和其函数
在同一上下文中
; - 3、
闭包包含该作用域下的所有变量/引用地址
; - 4、
定义函数不会创建闭包
,只有创建/执行函数同时才创建闭包;
注意:使用闭包时一定要注意其作用域!
如下图:第1行到第9行
,只涉及到变量的声明赋值
和函数的定义
,所以不会有闭包产生
;当代码执行到第10行
,执行函数fn创建函数实例
,因此会伴随着创建该作用域的一个闭包
,闭包中包含变量a和b;当第10行执行完,函数实例销毁(函数内部没有引用外部变量
),闭包也就随之销毁。
二、函数引用外部变量后的闭包
“一”中代码实例主要介绍了闭包和函数的创建,如果仅仅是这些描述,我们也没有必要去了解和使用闭包;但是,当函数中“引用”(借用)了外部的变量
后,一切都变得精彩了:
代码分析:
function makeAdder() {
var sum = 0
return function(y) {
sum += y;
return sum
};
}
var add1 = makeAdder();
var add2 = makeAdder();
console.log(add1(1)); // 1
console.log(add1(1)); // 2
console.log(add2(2)); // 2
console.log(add2(2)); // 4
分析下图:
- 1、
1-7
行定义了makeAdder函数
,并将函数定义存储在内存中(蓝色圈圈); - 2、第
9
行,调用makerAdder定义执行1-7行
代码,声明并赋值sum;将3-6行函数定义
返回并赋值给变量add1
,同时创建对应闭包
,存放sum变量,且值为0;第9行执行完毕,销毁本地执行上下文和sum变量
,控制权交给调用上下文; - 3、第
10
行,调用makerAdder定义执行1-7行
代码,声明并赋值sum;将3-6行函数定义
返回并赋值给变量add2
,同时创建对应闭包
,存放sum变量,且值为0;第10行执行完毕,销毁本地执行上下文和sum变量
,控制权交给调用上下文; - 4、由于add1和add2创建
两个新的函数实例,
所以其相对应闭包是相互不影响
的; - 5、执行到
12
行,调用add1实例并执行函数(3-6行)
,传入参数y为1,执行sum += y
,在查找本地或全局执行上下文之前,让我们检查一下闭包
,结果闭包包含一个名为sum的变量,sum变量从0变为1,同时返回sum,最终打印出1。执行完毕,销毁本地执行上下文; - 6、执行到
13
行,调用add1实例并执行函数(3-6行)
,传入参数y为1,执行sum += y
,在查找本地或全局执行上下文之前,让我们检查一下闭包
,结果闭包包含一个名为sum的变量,sum变量从1变为2,同时返回sum,最终打印出2。执行完毕,销毁本地执行上下文; - 7、执行到
15
行,调用add2实例并执行函数(3-6行)
,传入参数y为1,执行sum += y
,在查找本地或全局执行上下文之前,让我们检查一下闭包
(此时的闭包和add1的闭包处于不同函数实例,故相互不不影响
),结果闭包包含一个名为sum的变量,sum变量从0变为2,同时返回sum,最终打印出2。执行完毕,销毁本地执行上下文; - 8、执行到
16
行,调用add2实例并执行函数(3-6行)
,传入参数y为1,执行sum += y
,在查找本地或全局执行上下文之前,让我们检查一下闭包
,结果闭包包含一个名为sum的变量,sum变量从2变为4,同时返回sum,最终打印出4。执行完毕,销毁本地执行上下文;
在全局作用域中创建的函数创建闭包
,但是由于这些函数是在全局作用域中创建
的,所以它们可以访问全局作用域中的所有变量
,闭包的概念并不重要
。当函数返回函数
时,闭包的概念就变得更加重要了。返回的函数
可以访问不属于全局作用域的变量
,但它们仅存在于其闭包
中。
三、循环中闭包让人很意外
标题所说的循环是指在for循环的代码块中使用闭包所带来的的烦恼:
function starfunc() {
var tipText = [
'hello tom',
'hello jerry',
'hello jack'
];
for (var i = 0; i < tipText.length; i++) {
var item = tipText[i];
setTimeout(() => {
console.log(item);
}, 100);
}
}
starfunc();
来,猜一猜最终会输出什么?按照期望,我们想最终打印出来的是hello tom;hello jerry;hello jack
,但是实际上会连续打印三次hello jack
为什么呢?这里会涉及到执行上下文(全局作用域、函数作用域、块级作用域)和事件循环相关内容(传送门)(默认大家都会哈)。
因为for
循环体中变量 item
是var
声明的,所以会变量提前
到starfunc函数的顶部
,整个函数内部是一个函数作用域/执行上下文
;
- 1、根据
事件循环
规则,执行到第10行
时,setTimeout
进入调用栈
等待,回调函数
会进入Event Table
执行,在回调函数创建时生成闭包
,将item
存入; - 2、当
i
为0
时,闭包中item
为hello tom
; - 3、
for
循环继续,当i
为1
时,闭包中item
为hello jerry
; - 4、
for
循环继续,当i
为2
时,闭包中item
为hello jack
; - 5、
循环结束
,主线程执
行完毕出现空闲
时间,调用栈
的Event Queue
中依次打印三次item
,而item
则是从闭包中获取
,这就是为什么最终输出三遍hello jack
;
那有什么方法解决呢,请看代码:for
循环体中变量 item
使用let声明,此时starfunc
函数是一个函数作用域
,而每次for循环
时,都会创建一个块级作用域
,产生不同的执行上下文
,各个上下文
单独管理自己的变量:
- 1、根据
事件循环
规则,执行到第10行
时,setTimeout
进入调用栈
等待,回调函数
会进入Event Table
执行,在回调函数创建时生成闭包
,将item
存入; - 2、当
i
为0
时,单独作用域产
生自己的执行上下文
,该上下文
对应闭包中item
为hello tom
; - 3、
for
循环继续,当i
为1
时,单独作用域
产生自己的执行上下文
,该上下文
对应闭包中item
为hello jerry
; - 4、
for
循环继续,当i
为2
时,单独作用域
产生自己的执行上下文
,该上下文
对应闭包中item
为hello jack
; - 5、
循环结束
,主线程执
行完毕出现空闲
时间,调用栈
的Event Queue
中依次打印三次item
,而item
则是从三个不同互不影响的闭包中获取
,最终输出hello tom;hello jerry;hello jack
;
四、闭包的性能
如果不是某些特定任务需要
使用闭包,在其它函数中创建函数是不明智
的,因为闭包在处理速度
和内存消耗方面
对脚本性能具有负面影响
。例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次
(也就是说,对于每个对象的创建,方法都会被重新赋值)。
示例:
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;
};
五、小结
闭包的使用涉及到很多方面,例如框架中数据的双向绑定
,函数的私有属性
等;了解闭包的相关知识一是为了在开发中避免因为闭包带来的不利影响
、二则是要善于利用闭包的特性解决实际问题
!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。