资讯专栏INFORMATION COLUMN

词法作用域与闭包

张金宝 / 2133人阅读

摘要:我们可以这么解决问题在上述代码中,每次遍历我们都使用生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

首先需要明确的是,javascript 中没有块作用域,只有函数作用域
到底什么是闭包?
function foo() {
    var a = 2;

    function bar() {
        console.log(a); // 2
    }

    bar();
}

foo();

这是闭包吗?
技术上来讲也许是,但确切地说并不是。

我认为最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却是非常重要的一部分!)
function foo() {
    var a = 2;

    function bar() {
        console.log(a); // 2
    }

    return bar;
}

var baz = foo();

baz(); // 2

这才是闭包的效果!
在上例中函数 bar 在定义自己的词法作用域以外的地方执行,在 foo 执行后垃圾回收器不知道 bar 什么时候会被调用,这使得对 foo 内部作用域的回收工作被阻止,而 bar 依然持有对该作用域的引用,这个引用就叫做闭包

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
循环与闭包

要说明闭包,for 循环是最常见的例子。

for (var i = 0; i < 10; i++) {
    console.log(i);
}

执行上述代码我们可以打印出0到9十个数字,但是如果执行下面这段代码呢?

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

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。

我们可以这么理解问题:众所周知 javascript 是解释型的单线程编程语言,上述代码的执行过程实际是,执行 for 循环并把5个匿名函数的触发分别间隔 1-5 秒放入异步队列当中,而当异步队列的头节点开始执行时,for 循环早已经执行完毕,并且此时 i 作为全局作用域中的变量,其值为 6,这时无论异步队列中的头节点还是尾节点,在打印 i 变量的值并试图向上层作用域查找 i 时,获得的结果都是 6。

我们可以这么解决问题!

for (var i = 1; i <= 5; i++) {
    (function(j) {
        setTimeout(function timer() {
            console.log(j);
        }, j * 1000);
    })(i);
}
在上述代码中,每次遍历我们都使用 IIFE 生成一个新的作用域,使得延迟函数的回调可以将新的
作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

用容易理解的语言可以这么描述:IIFE 在执行完毕后,不知道延迟函数的回调什么时候执行,所以 IIFE 的作用域无法被销毁,其 j 变量的值被锁住。

块作用域与闭包

熟悉 ES6 的同学们都知道,下述代码也可以解决上面的 for 循环问题:

for (let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}
for (let i = 1; i <= 5; i++) 这行代码的圆括号之间存在一个隐藏的作用域,在每次执行循环体之前,javascript 引擎会把 i 在循环体的上下文中重新声明及初始化一次。

看到这儿我想大家应该有一个比较清楚的认识:解决 for 循环问题的根本是 块作用域!无论是 let/const 还是闭包,其本质都是在每次遍历时构建一个块作用域。

至少从 ES3 发布以来,javaScript 中就有了块作用域,而 withcatch 分句就是块作用域的两个小例子。

附录

Tip: let 存在提升,只不过由于暂时死区的限制,不能在 let x 之前使用 x。

let 的「创建」过程被提升,但是初始化没有提升

var 的「创建」和「初始化」都被提升

function 的「创建」「初始化」和「赋值」都被提升

const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没有「赋值」过程

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83211.html

相关文章

  • Js基础知识(三) - 作用域与闭包

    摘要:是词法作用域工作模式。使用可以将变量绑定在所在的任意作用域中通常是内部,也就是说为其声明的变量隐式的劫持了所在的块级作用域。 作用域与闭包 如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号? 看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,...

    lemanli 评论0 收藏0
  • Js基础知识(三) - 作用域与闭包

    摘要:是词法作用域工作模式。使用可以将变量绑定在所在的任意作用域中通常是内部,也就是说为其声明的变量隐式的劫持了所在的块级作用域。 作用域与闭包 如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号? 看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,...

    XFLY 评论0 收藏0
  • Js基础知识(三) - 作用域与闭包

    摘要:是词法作用域工作模式。使用可以将变量绑定在所在的任意作用域中通常是内部,也就是说为其声明的变量隐式的劫持了所在的块级作用域。 作用域与闭包 如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号? 看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,...

    tanglijun 评论0 收藏0
  • Js基础知识(三) - 作用域与闭包

    摘要:是词法作用域工作模式。使用可以将变量绑定在所在的任意作用域中通常是内部,也就是说为其声明的变量隐式的劫持了所在的块级作用域。 作用域与闭包 如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号? 看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,...

    lmxdawn 评论0 收藏0

发表评论

0条评论

张金宝

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<