module1-05-JS 闭包难点剖析
-
-
思考题:
-
① JS中的作用域是什么意思
-
② 闭包会在那些场景中使用
-
③ 通过定时器循环输出自增的数字通过JS的代码如何实现?
-
一、作用域、闭包介绍
1.1 作用域
-
在ES6出现之前只存在全局作用域与函数作用域
-
在ES6出现之后出现了块级作用域
console.log(a) //a is not defined
if (true) {
let a = '123';
console.log(a); // 123
}
console.log(a) //a is not defined
-
可以看出在if的{}里面使用 let 声明的a在 {} 之外就无法访问到
1.2 闭包
(1)红宝书和MDN上给出闭包的概念
红宝书闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。 MDN:一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
(2)闭包的基本概念
-
闭包其实就是一个可以访问到其他函数内部变量的函数,即一个定义在函数内部的函数
-
因为通常的情况下,函数内部变量是无法在外部访问的,
-
比如返回的函数能访问到fun1里面的a
-
function fun1() {
var a = 1;
return function(){
console.log(a);
};
}
fun1();
var result = fun1();
result(); // 1
(3)闭包产生的原因
-
这里面需要了解到作用域链的概念,而什么是作用域链呢?
-
当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,知道找到该变量或者不存在父级作用域中,这样的链路就是作用域链。
-
比如这里再fun2寻找a的时候,现在自身的作用域查找,没有的话区fun1,再去全局
-
-
var a = 1;
function fun1() {
var a = 2
function fun2() {
var a = 3;
console.log(a);//3
}
}
-
所以说明了 当前函数一般都会存在上层函数的作用域的引用,那么就形成了一条作用域链
-
所以产生闭包的实质就是,当前环境中存在指向父级作用域的引用
(4)闭包的不同出现情况
-
从上面的例子可以知道,闭包都是在一个函数中返回另外一个函数,那么如果没有返回的函数的
-
就比如换一种闭包的情况
-
var fun3;
function fun1() {
var a = 2
fun3 = function() {
console.log(a);
}
}
fun1();
fun3();
-
通过闭包的实质我们可以知道,只需要存在指向父级的引用即可,所以代码里面的fun3是一个存在与全局的变量,而再fun1里面,用了赋予一个匿名函数的创建方法,也可以获得指向父级的a
-
所以不管有没有放回函数,只需要满足条件即可
-
二、闭包的表现形式
-
在明白了闭包的本质之后,可以看看闭包的表现形式以及应用场景到底有哪些
(1)返回一个函数,上面已经讲过了
(2)在定时器,事件监听,Ajax请求、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
// 定时器
setTimeout(function handler(){
console.log('1');
},1000);
// 事件监听
$('#app').click(function(){
console.log('Event Listener');
});
(3)作为函数参数传递的形式
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
foo(); // 输出2,而不是1
(4)IIFE
-
创建了闭包,保存了全局作用域(window)和当前函数䣌作用域,因此可以输出全局的变量
-
这个函数会稍微有些特殊,算是一种自执行的匿名函数,这个匿名函数有独特的作用域,可以避免外界访问此IIFE中的变量,而且不会污染全局作用域
-
var a = 2;
(function IIFE(){
console.log(a); // 输出2
})();
三、使用闭包解决实际问题
(1)循环输出问题
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
-
这是一个非常经典的题目,相信我不用多说了
-
这段代码的输出结果是 5 个 6
-
提出疑问
-
① 为什么是5个6
-
② 怎么输出1、2、3、4、5
-
-
分析出现问题的原因
-
① setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
-
② 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。
解决问题
-
利用IIFE
for(var i = 1;i <= 5;i++){ (function(j){ setTimeout(function timer(){ console.log(j) }, 0) })(i) }
-
利用ES6中的let
-
最优解决
-
for(let i = 1; i <= 5; i++){ setTimeout(function() { console.log(i); },0) }
-
定时器传入第三个参数
-
很多人不知道其实setTimeout存在第三个参数的,第三个参数。即引用i此时的状态的值
-
for(var i=1;i<=5;i++){ setTimeout(function(j) { console.log(j) }, 0, i) }
四、总结
-
其实闭包的使用在日常的 JavaScript 编程中经常出现,使用的场景特别多而且复杂。由于闭包会使一些变量一直保存在内存中不会自动释放,所以如果大量使用的话就会消耗大量内存,从而影响网页性能。因此,你更应该深入理解闭包的原理,从而保证交付的代码性能更好。