闭包
1. 作用域
定义:通俗讲就是变量能够访问到的范围。
分类:全局作用域、函数作用域、块级作用域
1.1 全局变量&全局作用域
在JS中变量一般分为全局变量和局部变量。全局变量定义在函数外部代码最前面的。全局变量的挂载到window对象上的变量,在网页的任何位置都可以使用全局变量。在JS中没有定义直接赋值的 变量默认就是一个全局变量。全局变量拥有全局作用域。
缺点:变量污染,命名冲突
1.2 函数变量&函数作用域
在JS中函数中定义的变量,叫函数变量,这个时候只能在函数中使用,因此它的作用域也就是在函数内部,称为函数作用域。当这个函数执行完毕后会销毁这个函数变量。
function getName () {
var name = 'inner';
console.log(name); //inner
}
getName();
console.log(name);
1.3 块级作用域
ES6新增了块级作用域,最直接的表现就是let,使用let声明的变量只能在块级作用域中访问,有暂时性死区的特点,也就是在变量未声明之前不能使用。(其实就是在for、if块中使用的就是块级作用域)
2. 闭包
2.1 闭包概念
一个函数和对其周围状态的引用绑定到一起的组合就是闭包。也就是闭包可以让内层函数访问到外层函数的作用域。通俗的讲,闭包就是一个可以访问其他函数内部变量的函数,即一个定义在函数内部的函数,或者直接说闭包四个内嵌函数也可以。
因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。
function fun1() {
var a = 1;
return function(){
console.log(a);
};
}
fun1();
var result = fun1();
result(); // 1
2.2 作用域链
2.3.1 作用域链概念:
当访问一个变量时,代码解释器会首先在当前作用域找,如果没找到会去父级作用域找,直到找到该变量或没有父级作用域,这样的链就是作用域链。
var a = 1;
function fun1() {
var a = 2
function fun2() {
var a = 3;
console.log(a);//3
}
}
从中可以看出,fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。
那么这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。
由此可见,闭包产生的原因:当前环境中存在指向父级作用域的引用
2.3 闭包产生的原因
闭包产生的原因:当前环境中存在指向父级作用域的引用
function fun1() {
var a = 2
function fun2() {
console.log(a); //2
}
return fun2;
}
var result = fun1();
result();
从上面这段代码可以看出,这里 result 会拿到父级作用域中的变量,输出 2。因为在当前环境中,含有对 fun2 函数的引用,fun2 函数恰恰引用了 window、fun1 和 fun2 的作用域。因此 fun2 函数是可以访问到 fun1 函数的作用域的变量。
那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此还可以这么改代码,如下所示。
var fun3;
function fun1() {
var a = 2
fun3 = function() {
console.log(a);
}
}
fun1();
fun3();
可以看出,其中实现的结果和前一段代码的效果其实是一样的,就是在给 fun3 函数赋值后,fun3 函数就拥有了 window、fun1 和 fun3 本身这几个作用域的访问权限;然后还是从下往上查找,直到找到 fun1 的作用域中存在 a 这个变量;因此输出的结果还是 2,最后产生了闭包,形式变了,本质没有改变。
因此最后返回的不管是不是函数,也都不能说明没有产生闭包。
2.4 闭包的表现形式及应用场景
-
返回一个函数
-
在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
// 定时器 setTimeout(function handler(){ console.log('1'); },1000); // 事件监听 $('#app').click(function(){ console.log('Event Listener'); }); -
作为函数参数传递的形式
var a = 1; function foo(){ var a = 2; function baz(){ console.log(a); } bar(baz); } function bar(fn){ // 这就是闭包 fn(); } foo(); // 输出2,而不是1 -
IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量
var a = 2; (function IIFE(){ console.log(a); // 输出2 })();IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域
-
aaa
3. 如何解决循环输出问题?
在面试中,解决循环输出问题是比较高频的面试题,一般都会给一段这样的代码让你来解释
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
上面这段代码执行之后,从控制台执行的结果可以看出来,结果输出的是 5 个 6,那么一般面试官都会先问为什么都是 6?
可以围绕这两点来答:
- setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
- 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。
如何按顺序依次输出 1、2、3、4、5 呢?
1. 利用 IIFE
可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
从上面的代码可以看出,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。
2. 定时器传入第三个参数
setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。那么结合第三个参数,调整完之后的代码如下
for(var i=1;i<=5;i++){
setTimeout(function(j) {
console.log(j)
}, 0, i)
}
从中可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现我们想要的结果,这也是一种解决循环输出问题的途径。
注意点:
由于闭包会使一些变量一直保存在内存中不会自动释放,所以如果大量使用的话就会消耗大量内存,从而影响网页性能。