前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《你不知道的JavaScript》:闭包与局部作用域

《你不知道的JavaScript》:闭包与局部作用域

作者头像
前端_AWhile
发布2022-05-10 21:42:46
4980
发布2022-05-10 21:42:46
举报
文章被收录于专栏:前端一会前端一会

《你不知道的JavaScript》第一部分作用域和闭包第4篇。

在掌握作用域的前提下,才能真正理解和识别闭包。

闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

代码语言:javascript
复制
function foo(){
    var a = 10;
    return function(){
        console.log(a*2);
    }
}
var fn = foo();
fn();   //20  => 闭包的效果

上述示例中,fn函数的词法作用域能够访问foo()函数的内部作用域。foo()函数以一个函数对象作为返回值。在foo()函数执行后,其返回值被赋值给了变量fn并调用fn()。实际上只是通过不同的标识符引用调用了这个被作为返回值的函数对象。

fn()可以被正常执行,并且还是在被自己定义的词法作用域之外执行。

这就是闭包的神奇了。通常一个函数在执行完毕后其内部作用域就会被销毁,但由于内部作用域此时仍然被标识符fn所指向的函数对象所引用,所以foo()函数的内部作用域不会被销毁,而这个引用就是闭包。

关于闭包,概念拎的差不多,再说也就那样,这玩意还是要能真悟到,不然还是会想不通的。

下面说个闭包的典型应用,for循环。

代码语言:javascript
复制
for(var i=1; i<=5; i++){
    setTimeout(function timer(){
        console.log(i)
    }, i*1000)
}
//输出 => 6 6 6 6 6

上例结果是以每秒一次的频率输出6,而非设想的每秒一次的输出数字1~5。

出现这样非预期的情况,原因如下:

  • js是单线程,只有同步代码执行完毕后,才会去执行异步代码。由于setTimeout是异步的,所以每次for循环时js都会挂起setTimeout这个异步任务,等到for循环这个同步任务执行完毕时,系统才会执行异步的任务队列,即执行setTimeout的回调函数。而当for循环执行完毕后,变量i的值就是6。
  • 虽然for循环出来了五个延迟函数,但是根据作用域的工作原理,这五个延迟函数是被封闭在for循环所处的全局作用域内的。这个全局作用域中的变量i此时值已是6。所以五个延迟函数在执行时都读到同一个值为6的变量i,最后结果最后打印结果也就是五个6。

理解上面的原因,需要理解两个知识点:js中同步异步的执行顺序原理、作用域的工作原理。

找出了问题的原因,解决办法也就浮出水面了。如果能够让每个延迟函数time()都处于一个局部作用域中,并且该局部作用域中存在相应的变量i,让延迟函数timer()来访问该变量,不就行了么?创建局部作用域可以使用匿名函数自执行(IIFE)来做。

好,来搞个

代码语言:javascript
复制
for(var i=1; i<=5; i++){
    (function(){
        setTimeout(function timer(){
            console.log(i);
        }, i*1000)
    })()
}

一打印,哦哟,咋还是以每秒一次的频率输出6哇,这不是已经把每个延迟函数单独放进一个局部作用域中了么?

再仔细一看,局部作用域里是空的,没有变量i,逼的timer()只能访问外层作用域中的变量i,所以此时需要将外层作用域中每次循环出来的变量i都传进每个局部作用域中去:

代码语言:javascript
复制
for(var i=1; i<=5; i++){
    (function foo(j){
        setTimeout(function timer(){
            console.log(j)
        }, j*1000)
    })(i)
}

完美,此时就能顺利的达到预期设想了:每秒一次的输出数字1~5。

再回过头想想这个,正确执行的原理就是:先是按照同步异步执行原理,先执行同步操作,即执行每个for循环,并将for循环出来的每个i值传入foo自执行函数中,foo自执行函数形成一个局部作用域,循环多少次就有多少个foo自执行函数局部作用域,每个局部作用域中的 i 值按循环顺序排列。当for循环这个同步执行完毕后,没有其他同步代码的情况下,引擎再执行异步队列中的所有time()定时器,每个timer()定时器都可以获取各自所处局部作用域中的 i 值,这里注意 i 值已被赋值给了变量 j 。

想到块作用域,ES6中的let关键字不是可以主动生成块作用域的么,把上例改一下,可以更简便的实现预期设想:

代码语言:javascript
复制
for(let i=1; i<=5; i++){
    setTimeout(function timer(){
        console.log(i)
    }, i*1000)
}
//输出 => 1 (1s) 2 (1s) 3 (1s) 4 (1s) 5

完美!

块作用域 + 闭包,简直不要太如鱼得水。

闭包的作用强大,还可以用来写模块。

代码语言:javascript
复制
function foo(){
    var name = "nitx",
        skills = ['frontend', 'backend', 'db'];

    function showName(){
        console.log(name);
    }

    function showSkills(){
        var skillStr = skills.join("、");
        console.log(skillStr);
    }

    return {
        showName: showName,
        showSkills: showSkills
    }
}

var fn = foo();
fn.showName();
fn.showSkills();

/*
打印 =>
nitx
frontend、backend、db
*/

函数foo()在调用后形成一个闭包,其返回值是一个对象字面量,其作用是模块暴露,将其赋值给了一个模块实例标识符fn,通过fn来调用模块API方法。

上面这个示例中,每当foo()调用一次,就会生成一个模块实例,如果设想只能生成一个模块实例,可以使用IIFE来实现:

代码语言:javascript
复制
var fn = (function foo(){
    var name = "nitx",
        skills = ['frontend', 'backend', 'db'];

    function showName(){
        console.log(name);
    }

    function showSkills(){
        var skillStr = skills.join("、");
        console.log(skillStr);
    }

    return {
        showName: showName,
        showSkills: showSkills
    }
})()
fn.showName();
fn.showSkills();

结果也是一样的。

这就是闭包的模块应用,当然在ES6版本后,官方提供了正规的模块模式,importexport等,这个可以自行去看相关资料,总之是很好用的。

最后来总结下闭包吧。

当函数可以始终记住并访问其所在作用域,即使该函数在该作用域之外执行,这种情况就产生了闭包。

闭包的形成需要两要素:存在访问目标的局部作用域、函数始终保持对该作用域的引用。

能用闭包的形式实现的,也可以用面向对象写法实现,反之亦然。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-01-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档