一文搞懂 JavaScript 执行上下文 + 变量对象 + 作用域链 + 闭包

闭包是JavaScript编程中的一个重要概念,几乎每个前端面试都会考察。掌握理解好闭包这个概念对于前端开发来说相当重要。然而想要真正透彻地理解这一概念,还需要掌握诸多前置概念,如:执行上下文,作用域链以及变量对象。网络上也不乏讲解闭包原理的优秀文章,笔者在查阅众多资料之后结合自己的理解,力求用简明清晰而又不失深度的图文来向大家讲明这个概念。

 
 

执行上下文


要理解什么是执行上下文,首先得明确一下 scope 以及 context 这两个概念。

每一个函数调用(激活)都会创建相应的 scope 与 context 。它们都指定了一个作用区域。
- scope 指的是 函数被调用的时候, 各个变量的作用区域。
- context 指的是当前 scope 和包裹它外面的scope ( current scope and its enclosing scope )。

下面看看一段 JavaScript 代码以及相应的作用区域结构,

var x = 1;
function foo () {
    var x = 2;
    return function () { return x + 1; }
}

var res = foo();
res();

一文搞懂 JavaScript 执行上下文 + 变量对象 + 作用域链 + 闭包_第1张图片

所谓执行上下文(Execution Context),就是指当前解释器执行代码对应的上下文。而不同运行环境的执行上下文并不不同,JavaScript是如何管理多个执行上下文的呢?

JavaScript的运行环境分类:
1)全局环境
2)函数环境
3)eval

实际上,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。

 

函数调用栈

解释器执行代码进入新的运行环境时都会生成一个新的执行上下文,随后将其push进函数调用栈(call stack)。栈底永远为全局上下文,而栈顶就是当前执行的上下文。退环境时将其出栈。

下面分析一下前面代码的函数调用栈行为:
一文搞懂 JavaScript 执行上下文 + 变量对象 + 作用域链 + 闭包_第2张图片

  1. 在客户端环境下,打开浏览器,解释器最先将全局上下文入栈;
  2. 调用函数foo(),由全局环境进入函数环境,将函数 foo 的上下文入栈;
  3. 遇到 return 语句,跳出当前函数环境回到全局环境,将函数 foo 的上下文退栈;
  4. 调用函数res(),由全局环境进入函数环境,将函数 res 的上下文入栈;
  5. 遇到 return 语句,跳出当前函数环境回到全局环境,将函数 res 的上下文退栈;

注意一点,函数声明不会改变运行环境,从而不会改变函数调用栈。

到这里,我们大致了解了执行上下文从入栈到出栈这一行为以及相应的触发条件。但这还不够,为了深入学习后续概念,我们还得更进一步地了解执行上下文的生命周期。这里借用波老师的一张图来阐释:

一文搞懂 JavaScript 执行上下文 + 变量对象 + 作用域链 + 闭包_第3张图片

可以看到,执行上下文创建伊始首先会生成变量对象,然后再建立作用域链。下节将阐述变量对象这一概念,这也是理解作用域链的基础。

 

变量对象

变量对象持有相应执行上下文的里声明所有函数、变量以及参数的引用或值。在未进入到执行阶段前,变量对象不可被访问,进入到执行阶段后,变量对象转变为活动对象,方才接受访问。

创建过程:

  1. 建立 arguments 对象
  2. 检查 function 函数声明创建属性
  3. 检查 var 变量声明创建属性
var f1 = 1; // 变量声明会被提升,但赋值操作不会
function f1 () {};
console.log(f1); // 输出1,将上述两条声明调换位置,得到同样的结果

// 再看看这段代码,从另一层面上解释了上段代码
function f2 () {};
f2 = 1;
console.log(f2); // 输出1

function f3 () {
    function f2 () {};
    return function () { console.log(f2) };
}

f3()(); // 输出 function f2 () {},内层 scope 的声明会覆盖外层 scope 的同名声明

Note: 写完前两个代码示例我对函数声明与变量声明之间的关系又有了一点特别的理解。函数声明其实可以看做是连着函数赋值一起被提升的变量声明
举个例子,function f () {} 就相当于连着赋值操作一起被提升的 var f = function () {} 。哈哈,有意思。

全局上下文的变量对象

全局上下文与其他上下文不同的一点在于: 其变量对象以及 this 都指向 window 对象。

作用域链

定义:是由当前环境与上层环境的一系列变量对象组成,保证了当前执行环境对符合权限的变量和函数的有序访问。

var a = 1;
var b = 2;

function foo () {
    var a = 1;
    function innerFoo () {
        var c = 3; 
        return a + b + c; 
    };
    return innerFoo;
}

var outerFoo =  foo();
outerFoo();

下面给出 innerFoo 的作用域链:

一文搞懂 JavaScript 执行上下文 + 变量对象 + 作用域链 + 闭包_第4张图片

闭包

概念(基于 chrome )

闭包是一种特殊的对象,它由两部分组成。
执行上下文(代号 A ),以及在该执行上下文中创建的函数(代号 B )。
执行 B 时若访问了 A 中变量对象的值,那么就产生了闭包。在 Chrome 中,用 A 的函数名指代闭包。

函数 A 在执行完毕后,生命周期结束,其执行上下文就会失去引用,占用的内存空间也会被垃圾回收器释放。然而闭包的存在阻止了这一切。

闭包的主要特征是可以在其他执行上下文里访问到其变量对象也就是函数的局部变量。

你可能感兴趣的:(前端基础)