JavaScript基础专题之闭包(四)

2019-07-04  本文已影响0人  Chris__Liu

定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

什么又是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

举个例子:

var a = 0; //自由变量

function foo() {
    console.log(a);//访问自由变量,此时这个变量并不是函数参数或者函数的局部变量
}

foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以我们说 a 就是自由变量,那么函数 foo 就形成了一个闭包。

所以在《 JavaScript权威指南 》中讲到:从技术的角度讲,所有的 JavaScript 函数都是闭包。

在ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

接下来就来讲讲实践上的闭包。

常见的闭包问题

以下代码为什么与预想的输出不符?

// 代码1
for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 输出5次5
    }, 0)
}

假设A:因为 setTimeout 这块的任务直接进入了事件队列中,所以 i 循环之后i先变成了5,再执行 setTimeoutsetTimeout 中的箭头函数会保存对i的引用,所以会打印5个5.

// 代码2
for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 输出 0,1,2,3,4
    }, 0)
}

假设结论 A 成立,那么上式应该也是输出5次5,但是很明显不是,所以结论A并不完全正确。

那我们去掉循环,先写成最简单的异步代码:

function test(a){
    setTimeout(function timer(){
        console.log(a)
    },0)
}
test('hello')

复制代码执行 testsetTimeouttimer 函数放入了事件队列,timer 保留着 test 函数的作用域(在函数定义时创建的),test 执行完毕,主线程上没有其他任务了,timer 从事件队列中出队,执行 timer,执行 console.log ( a ) ,由于闭包的原因,a 依然会保留着之前的引用,输出 'hello'

那我们在回到题目中,因为两段代码中的不同只有声明语句,所以我们提出假设B :因为在代码1中,匿名函数保留着外部词法作用域,i 都是在全局作用域上,代码2中由于存在块作用域,所以它保留着每次循环时i的引用。

// 代码3
for (var i = 0; i < 5; i++) {
    ((i) => {
        setTimeout(function timer() {
            console.log(i) // 输出 0,1,2,3,4
        }, 0)
    })(i)
}

复制代码使用 IIFE 传递了变量i给匿名函数,IIFE 产生了一个新作用域,timer中保留对匿名函数中的i的引用,所以会依次输出。

// 代码4
for (var i = 0; i < 5; i++) {
    (() => {
        setTimeout(function timer() {
            console.log(i) // 输出 5个5
        }, 0)
    })()
}

代码3的区别为IIFE 没有给匿名函数传递 i,timer 保留的作用域链中对i的引用还是在全局作用域上。

经过以上两个变体的验证,所以假设B 成立,即:由于作用域链的变化,闭包中保留的参数引用也发生了变化,输出的参数也发生了变化。

下例,循环中的每个迭代器在运行时都会给自己捕获一个i的副本,但是根据作用域的工作原理,尽管循环中的五个函数分别是在各个迭代器中分别定义的,但是它们都会被封闭在一个共享的全局作用域中,实际上只有一个i,换句话说,i的值在传入内部函数之前,已经为 6 了,所以结果每次都会输出 6 。

for(var i=1; i <= 5; i++){
    setTimeout(function(){
        console.log(i);//6
    },0)
}

解决上面的问题,在每个循环迭代中都需要一个闭包作用域,下面示例,循环中的每个迭代器都会生成一个新的作用域。

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

也可以使用let解决,let声明,可以用来劫持块作用域,并且在这个块作用域中生明一个变量。

for(let i=1; i <= 5; i++){
    setTimeout(function(){
        console.log(i);
    },0)
}

总结

简单的说:函数 + 自由变量就形成了闭包。其实并不是特别复杂,只是我们需要在引用自由变量的时候小心作用域的变化。

JavaScript基础系列目录地址:

JavaScript基础专题之原型与原型链(一)

JavaScript基础专题之执行上下文和执行栈(二)

JavaScript基础专题之深入执行上下文(三)

新手写作,如果有错误或者不严谨的地方,请大伙给予指正。如果这片文章对你有所帮助或者有所启发,还请给一个赞,鼓励一下作者,在此谢过。

上一篇下一篇

猜你喜欢

热点阅读