继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

JavaScript作用域与闭包

为爱心太软
关注TA
已关注
手记 151
粉丝 1.4万
获赞 860

作用域

任何 JavaScript 代码在执行前都有大量的工作要做,如果只靠引擎自己,其实很难做到,前面提到过引擎的一个得力助手:作用域,在整个代码的编译阶段,发挥了非常大的作用。
作用域为引擎提供了环境内每一个标识符的位置信息,引擎依赖这些信息可以迅速查找到它们定义的位置。标识符的位置信息,是你在写代码时将标识符写在哪里来决定的,在词法分析阶段( 编译中的第一个阶段 ),这些标识符的位置信息就会以有序列表的形式保存到环境的 scope 属性中( 也就是作用域 ),以供引擎使用。也可以简单理解为,作用域里面保存的信息,在你写代码的时候已经决定了,而且会一直保持这个作用域不变。

例子:

var a = 1;

function afn() {
    var b = a + 1;
    var m = 999;

    function bar(c) {
        var n = a + b + c;
        console.log(n);
    }
    bar(3);
}
afn();
//输出:6

示意图:

图片描述

(1) 标识符 afn 和 a 位于全局作用域。
(2) 标识符 b、m 和 bar 位于 afn 所创建的作用域。
(3) 标识符 c 和 n 位于 bar 所创建的作用域。

我们可以通过浏览器,直观的看一下作用域的形式:

全局作用域:

图片描述

afn 所创建的作用域:

图片描述

bar 所创建的作用域:

图片描述

作用域的查找规则

1、查找标识符的过程会始终从当前作用域开始,然后逐级地向外层嵌套的作用域展开,直到找到标识符,或抵达最外层的作用域(也就是全局作用域)为止,如果找不到标识符,通常会导致错误发生。
2、每个执行环境都可以进入到外层作用域中查找标识符,但不能进入到内层作用域中查找标识符。

示意图:

图片描述

*有关于作用域基础性的介绍,可以参考我的另一篇手记:JavaScript作用域与作用域链

闭包

首先,闭包是一个函数;其次,也是最重要的一点,这个函数会一直保持对定义时所处作用域的引用。
下面,我们举例说明这个定义:

例子:

function fn() {
    var a = 1;

    function childFn() {
        console.log(a);
    }
    childFn();
}
fn();
//输出:1

上例中,内部函数 childFn 可以访问外部作用域中的变量 a,我相信大家通过作用域的查找规则,可以很容易理解。这个例子可能看起来更像函数的嵌套,那么,函数 childFn 算是闭包吗?函数 childFn 是完全符合闭包的两点定义的,所以属于闭包,只是不像"标准"的闭包。下面,我们简单修改,把它变成"标准"的闭包。

例子:

function fn() {
    var a = 1;

    function childFn() {
        console.log(a);
    }
    return childFn;
}

var bar = fn();
bar();
//输出:1

在修改后的例子中,我们将内部函数 childFn 当作返回值,在 fn 执行后,其返回值(也就是内部的 childFn 函数)赋值给变量 bar 并调用 bar,bar 被正常执行。这是一个我们最常见的标准闭包。

真正理解闭包

真正理解闭包的过程,也可以说是对作用域工作原理更加深入的探索。简单回顾一下作用域:在你书写一个函数的时候,这个函数和上下文的位置关系已经确定( 除非你重写 ),当调用这个函数的时候,函数会携带这些位置信息,复制给环境中的 scope 属性。因此,无论内部函数是在定义的作用域中被调用,还是在定义的作用域以外的地方被调用,属性 scope 中保存的信息都是一样的。
我们可以通过浏览器,直观的看一下:

视图1:内部函数在定义的作用域中被调用
图片描述

视图2:内部函数在定义的作用域以外的地方被调用
图片描述

*通过浏览器可以发现,不论内部函数 childFn 是在定义的作用域中被调用,还是在定义的作用域以外的地方被调用,属性 scope 中保存的信息都是一样的。

作用域不会被销毁

我们知道, scope 属性中只是保存了标识符的位置信息,而不是标识符的副本,因此每一个标识符指向的变量或函数都应该是真实存在的。但是,如果内部函数作为闭包被返回,却不知道何时何地才会去执行,此时,可能它的父函数早就已经执行完毕,父函数中定义的变量或函数应该已经不复存在。但实际上,被返回的闭包函数依然可以访问到它父函数中的变量或函数。

例子:

function fn() {
    var name="Tom";

    function childFn() {
        console.log(name);
    }
    return childFn;
}

var person = fn();
person();
//输出:Tom

在 fn 执行后,我们通常会认为 fn 的作用域会被销毁,变量 name 也会被清除,但闭包阻止了这件事情的发生。之所以会这样,和 JavaScript 的垃圾回收机制有很大的关系:JavaScript 的垃圾收集器对于正处于环境中的标识符以及环境中的标识符引用的变量或函数( 包括变量或函数定义时的作用域 ),不会进行清除,而是仍然留在内存中。

也许,你还会有这样一个疑问,代码都是一行一行地在执行,前面声明的变量或函数,怎么判断在后面的代码中是否还在使用呢?代码的确是逐行执行,但代码的编译却不是逐行进行的,而是以 "<script>" 符号分割( 或许是更大的范围 ),按区域进行编译的。你可以这样理解,代码执行前,所有的标识符都已经存储在栈中;随着代码不断地向下执行,除了那些正处于环境中的标识符以及仍在被引用的变量或函数,其他无用的标识符都会被垃圾收集器清除。

回调函数就是闭包

根据闭包的定义,我们可以试一试,回调函数是否仍然持有对定义时作用域的引用。

例子:

function fn() {
    var n = 999;

    function childFn() {
        console.log(n);
    }
    return childFn;
}

function runFn(callBack) {
    callBack();
}
runFn(fn());
//输出:999

*fn 执行后,它的内部函数 childFn 作为参数传递到函数 runFn 中去执行,回调函数 childFn 仍然能够访问变量 n,因此,回调函数 childFn 是一个闭包。

除了以上同步任务中的回调函数,异步任务中的回调函数也同样是闭包。

例子:

function fn() {
    var n = 999;

    function childFn() {
        console.log(n);
    }
    return childFn;
}

function runFn(callBack) {
    setTimeout(function() {
        callBack();
    }, 2000)
}
runFn(fn());
//输出:999

*fn() 执行 2 秒后,它的内部作用域并没有消失,回调函数 childFn 依然能够访问变量 n。

综合以上内容,我们可以得出这样的结论,只要使用了回调函数,实际上就是在使用闭包。

*关于闭包基础性的介绍,可以查看我的另一篇手记:JavaScript闭包详解


运行时流程图

结合以上内容,JavaScript 的运行时流程图如下:
图片描述

这张图会根据内容的增加不断进行补充。


深入挖掘系列手记

浏览器理论(未更新)


如有错误,欢迎指正,本人不胜感激。

打开App,阅读手记
4人推荐
发表评论
随时随地看视频慕课网APP

热门评论

写的超级棒,很受教

查看全部评论