从一道经典前端面试题再来看闭包

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
     console.log(i)   
  }, i * 1000)  
}

上面这个内容会打印什么?

看过这题的都会知道答案,每隔一秒打印一个5,打印5次。如果我想将每一轮循环的i打印出来呢,很简单,将var替换成let;

这道题真的是考察闭包吗?

为什么要有闭包?

因为在JavaScript中,没有办法在函数外部访问到函数内部的变量对象。那么反之,有了闭包,我们可以在函数以外的任何地方访问到函数内部的变量对象。

(注意,我这里用的是变量对象,而不是某个变量,因为它是一个合集,准确的说,是包含了整个函数作用域。)

如何写闭包?

常见的闭包方式是:

function fn1() {
  var a = 1,
        b = 2;
  return function() {
       return a
  }    
}

var fn2 = fn1();
fn2();    // 1

这里fn1执行完成后,按理说,内部的a、b所在的作用域应该会销毁,但是因为闭包的存在,返回的匿名函数保留了对当前作用域的引用,因此我们可以在fn1执行完成之后,依然可以访问到fn1内部的变量a,这就是闭包的使用。

(注意,这里虽然只是return了a,但是变量b也在内存中,也没有销毁,因为闭包保存的不是某个变量,而是整个变量对象)

再来看一些其它闭包例子

function fn1() {
  var a = 1;  
  setTimeout(function() {
      console.log(a)  
  }, 1000 )  
}

fn1();

// 1

当fn1执行完成后,内部作用域并没有销毁,而是被setTimeout保留下来了,因此这也是闭包!

var a = 1, b = 2;

function () {}

.....

var btn = document.getElementById('btn');

btn.addEventListener('click', function() {}, false);

没错,这也是闭包!我用DOM2级方式给btn这个dom节点添加事件,尽管里面什么变量都没有引入,但依然保留着外界的变量对象,这也是闭包!

除了上面这些,还有吗?当然有了,比如每一个带callback回调函数的,都是用了闭包,再比如每一个模块导出的时候,一定会有闭包来访问一些内部的函数或者变量,这也是闭包!

好了,现在我懂了

那我们再来回看最初提的那个问题,思考一下

为什么原题中的代码没有达到我们期待的效果?

我们所期待的是,每一次for循环,我们都能保存一个i的副本,将它保留下来并传给setTimeout,我们每次循环都会重新定义这个函数,也就是说第一次循环和第二次循环中的setTimeout是不一样的(也就是说循环结束的时候,是有5个函数)。题中的代码也就等同于下面的代码:

for (var i = 0; i < 5; i++) {
  {  
      setTimeout(function() {
         console.log(i)   
      }, i * 1000)  
  }  
} 

setTimeout本身就是一个闭包,而且大括号提供了一个块级作用域,所以我们理想情况下很容易做到,但是却失败了,原因是什么?并不是闭包的问题,而是我们保存的这个i的副本,出了问题。它们都被封闭在一个共享的全局作用域中,实际上只有一个i,看似有了块级作用域,但是没起作用,因为是var声明的变量不存在块级作用域,因此循环结束的时候,“所有”的i,其实也就是一个i,就是5。

这道题的解题思路是什么?

其实就是让var声明的变量i保留在块级作用域内。

那么我们再来看,为什么用let能解决这个问题,很简单,let声明的变量有块级作用域,因此i有了5个副本,并且毫不相关,再配合setTimeout的闭包,我们成功了!

上面那个方法也等于下面这个

for (var i = 0; i < 5; i++) {
  {  
    let j = i; setTimeout(function() { console.log(j) }, j * 1000) } } 

还有没有别的方法了,如果不改变var,如何制造块级作用域?es5里虽然没有块级作用域,但是我们有模拟块级作用域的方法:函数作用域!

for (var i = 0; i < 5; i++) {
  var a = function(j) {
      setTimeout(function() {
         console.log(j)   
      }, j * 1000)  
  };
   a(i);
   a = null;
} 

这里为了避免变量a污染全局,最后将a赋值为null,当然了,也可以let a ;

但是这样写又有些繁琐,因为还要创建一个函数a,然后再销毁,那能否不这样呢?

IIFE!也就是立即执行函数。

for (var i = 0; i < 5; i++) {
  (function(j) {
      setTimeout(function() {
         console.log(j)   
      }, j * 1000)  
  })(i) 
} 

综合来看,这道题与其说是考闭包,不如说是考块级作用域的概念,如果硬要考闭包,不如不给代码,把需求告诉他,让他手写一个,这样才行吧。

对了,这里再补充一点之前提过的,当我用let替换var的时候,既然每次循环都是一个块级作用域,互相不干扰,那为什么i会一直自动加1呢,它是怎么记得上次循环是多少呢?

因为JavaScript引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

end

猜你喜欢

转载自www.cnblogs.com/yanchenyu/p/10038058.html