美文网首页
JavaScript基础专题之闭包(四)

JavaScript基础专题之闭包(四)

作者: Chris__Liu | 来源:发表于2019-07-04 18:04 被阅读0次

    定义

    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基础专题之深入执行上下文(三)

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

    相关文章

      网友评论

          本文标题:JavaScript基础专题之闭包(四)

          本文链接:https://www.haomeiwen.com/subject/udybfctx.html