javascript中的闭包

前言

  本篇是基于对 《你不知道的JavaScript(上卷)》中的第五章的总结理解。

 

不完全正确的概念

  简单通俗的说,函数内嵌函数就是闭包。但不完全正确,最重要的是内部函数执行时仍然持有外层作用域内的引用。

 

闭包解释

function outer() {
        var a = 2
        function inner(){
            console.log(a)
        }

        return inner
}

var fun = outer()
fun()

  上述代码就是一个闭包。

  1. 内部函数 inner 的作用域包括自身作用域外还向外涵盖,所以 inner 内可以使用 outer 内的变量 a;
  2. outer 函数将 inner 函数作为返回值。

  一般来说,outer 函数执行完后,内部的变量 a 本来要被垃圾回收,但是其内部的函数 inner 有在使用 变量 a,为了 fun(也就是 inner ) 能够何时何地的正确执行,所以变量 a 不会被回收(保留了outer内的作用域)。这就是闭包。再看:

var fun
    function outer() {
        var a = 2
        function inner(){
            console.log(a)
        }

        fun = inner
}

outer()
fun()

  上述代码也是闭包。将内部函数 inner 赋值给fun。为了 inner 能够正确执行,变量 a 没有被回收。

  所有,无论使用何种方法将内部函数传递到外层作用域,内部函数仍然保持着原始作用域的引用(没有被回收),就会产生闭包。

 

  再看其他:

function wait(num) {
        setTimeout(function timer(){
            console.log(num)
        }, 1000)
}
wait(1)

  这也是闭包,不管执行1秒后,timer 内部仍然引用着num, 因为 setTimeout 会持有对 timer 的引用,并没有 clear。所以 wait 内的作用域会保留下来。

  

function click(value) {
        $(selector).click(function(){
            alert(value)
        })
}

click('hello')

  这也是闭包,因为 $(selector).click 绑定的函数内持有 click 的value,而 $(selector).click 执行后并没有解绑,所以为了保证正确运行,会保留 click 内的作用域。

 

function outer () {
        var a = 2
        function inner () {
            console.log(a)
        }
        inner()
}
outer()

  上述代码,虽然函数内嵌了函数,但严格来讲并不是闭包。inner 执行后,并没有持有变量 a 的引用。outer 内的作用域可能已经被回收了。

 

经典问题

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

  上述代码会打印 5 次 6。

  首先,javascript是单线程的,所以setTimeout(异步)会等for循环完了才会执行,而 for 循环完时, i 的值是6。但这并不足以导致setTimeout每次都打印出 6 。

  更重要的是每个(5个)setTimeout对 i 的引用,因为 for 中使用 var i 声明,所以 i 是在全局作用域内,每个setTimeout都引用了同一个 i。

  所以才会每次都打印出 6。

  相当于:

    var i = 6

    setTimeout(function timer(){
        console.log(i)
    }, i )

    setTimeout(function timer(){
        console.log(i)
    }, i )

    setTimeout(function timer(){
        console.log(i)
    }, i )

    // .....

 

 

  • 解决方案

    只要隔离每个setTimeout对 i 的引用即可。

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

  上面三种本质都是隔离每个setTimeout对 i 的引用。IIFE内自成一个作用域,将 i 赋值给其他变量,就隔离了对 i 的引用了。

  而使用 let,虽然 let 没有用在 {...}内,但 let 在for头部中有特殊行为,这个行为会将 i 多次声明,每迭代一次,声明一次,每次迭代都会使用上一次迭代结束时的值来初始化 i 。

  相当于:

    // 这里块作用域指 IIFE 内的作用域
    {
        var j = 1
        setTimeout(function timer(){
            console.log(j)
        }, j )
    }
    // 这里块作用域指 IIFE 内的作用域
    {
        var j = 2
        setTimeout(function timer(){
            console.log(j)
        }, j )
    }
    // ......

    /////////////////////////////////

    {
        let i = 1
        setTimeout(function timer(){
            console.log(i)
        }, i )
    }

    {
        let i = 2
        setTimeout(function timer(){
            console.log(i)
        }, i )
    }
    // ......

 

posted @ 2021-02-26 01:19  blogCblog  阅读(55)  评论(0编辑  收藏  举报