module1-05-JS 闭包难点剖析

JS 闭包难点剖析

  • javaScript中闭包是相当重要的概念,并且与作用域相关知识的指向密切相关。

  • 思考题:

    • ① 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 编程中经常出现,使用的场景特别多而且复杂。由于闭包会使一些变量一直保存在内存中不会自动释放,所以如果大量使用的话就会消耗大量内存,从而影响网页性能。因此,你更应该深入理解闭包的原理,从而保证交付的代码性能更好。

posted @ 2021-02-27 15:07  叻仔猪  阅读(89)  评论(0编辑  收藏  举报