likes
comments
collection
share

作用域、作用域链、执行上下文、执行上下文栈、变量对象、活动对象全了解

作者站长头像
站长
· 阅读数 11

JavaScript的学习有几个难点,其中讨论比较多的就有闭包和this,而要想更深入了解它们,那这篇内容是很有必要去学习的。

作用域(Scope)

作用域规定了某些特定部分中变量、函数和对象的可访问性,通俗点说就是在一个范围内可以访问某些变量,函数或者对象,当出了这个范围之后就无法访问了。

function scope() {
    let number = 100;
    console.log(number);
}
scope(); // 100 (在scope()范围内,可以访问)
console.log(number); // 报错:Uncaught ReferenceError: number is not defined(不在scope()范围内,无法访问)

javascript的作用域

javascript的作用域分为全局作用域和局部作用域。

其中全局作用域为 window (浏览器中)和 global (nodejs中)。 局部作用域又有函数作用域、块作用域(ES6)。

作用域分类 作用域分为词法作用域(静态作用域)和动态作用域。

词法作用域(Sexical Scope),也叫静态作用域(Static Scope),它的作用域是指在词法分析阶段就确定了,不会改变。

动态作用域(Dynamic Scope)是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。

JavaScript采用的是词法作用域,下面我们可以举个例子看一下:

let number = 100;

function scope() {
    console.log(number)
}

function numFun() {
    let number = 200;
    scope();
}
scope(); // 100

答案为什么是100而不是200?如果当我们从词法作用域这一方面去分析就会很容易理解了。

从上面我们知道词法作用域在词法分析阶段就确定了,换句话说当你写完函数的时候就确定了作用域。当我们调用到这个函数的时候它会先从函数作用域里面查找有没有这个值,有的话就输出,没有的话就就根据书写的位置去找它上一层的作用域是否存在这个值,如果有则输出出来。这个值和numFun()没有任何关系,numFun()只是负责调用了scope()这个函数而已。

但是如果是动态作用域又不一样了,因为检测当前作用域没有值之后它并不是去书写的位置查找,而是调用这个函数的作用域里查找,也就是numFun()里面查找所以动态作用域输出的值是200

作用域链(scope chain)

作用域链:函数对象有一个仅供 JavaScript 引擎使用的[[scope]] 属性,将[[scope]] 属性指向函数定义时作用域中的所有对象集合,这个集合就被成为作用域链,它是在执行期上下文的创建阶段被创建出来的。

作用域链用途是保证对执行环境有权访问的所有变量和函数的有序访问,换句话说就是如果想要使用一个变量或者一个函数,需要可以通过作用域链去找到它,否则就会报错。

因为JavaScript采用的是词法作用域,所以它的作用域在函数被创建的时候就已经确定(欺骗词法作用域这里不讨论),这个作用域它是包含着上级作用域的,它的规则已经确定它可以访问上级甚至上上级的作用域的变量,但是它还不是我们通常说的作用域链,或者说不尽然是。

在作用域的最前端始终是当前的执行上下文,末端始终是全局执行上下文(欺骗词法作用域这里不讨论)

function scope() {
    var a = 10;
    return a
}
scope()

上述例子中我们可以这么表示作用域链

// scope函数的执行上下文(创建阶段)
scopeEC = {
    VO: {...} // 变量对象
    scopeChain: [VO(scope),VO(global)], // 作用域链
}

当函数scope()运行时会创建执行上下文,而执行上下文的创建阶段又会将作用域链创建(复制函数[[scope]]属性作为初始化的作用域链),接着将变量对象添加到作用域链的前端。现在这个作用域链和函数被创建时存在的就不太一样了,它还包含了自己的变量对象和父级执行上下文的其他变量对象,当函数去查找一个变量的时候就会通过作用域链去找到相关的变量或函数(标识符)。

执行上下文(Execution Context)

当JavaScript首次解析代码的时候或者执行函数时会进入执行上下文。执行上下文可以看做是JavaScript代码运行的一个环境,所有的JavaScript代码都是在执行上下文中运行的。

函数每次执行时的执行环境都独一无二。多次调用函数就多次创建新的执行环境。

执行上下文分为:全局执行上下文、函数执行上下文,eval执行上下文(基本用不到,可忽略)。

执行上下文的生命周期

执行上下文的生命周期我们可以分成两个阶段:创建阶段,执行阶段。

以上面的的scope函数为例,执行上下文的生命周期是这样的:

  1. 创建阶段

    1. 生成变量对象
    2. 创建作用域
    3. 确定this的指向
  2. 执行阶段

    1. 变量对象转变为活动对象(完成变量赋值,函数引用)
    2. 执行其他代码

执行上下文栈(Execution Context Stack)

javascript是一门单线程语言,函数每次执行时的执行环境都独一无二。多次调用函数就多次创建新的执行环境。为了便于管理,JavaScript 引擎创建了执行上下文栈(Execution context stack),来管理执行上下文。

当一个函数被调用的时候就会创建一个执行上下文,而这时候执行上下文栈就会将这个函数的执行上下文压入栈中。

下面我们举个例子并将它们在栈中的存在形式用画图的方式表现出来。

function context1() {
    console.log('context1');
}
function context2() {
    context1();
    console.log('context2');
}
context2(); // context1 context2

作用域、作用域链、执行上下文、执行上下文栈、变量对象、活动对象全了解

从代码上我们知道它是先执行context2()函数,然后执行context1()函数。所以先创建了context2()函数的执行上下文,然后将其压入栈中,接着调用context1()函数的时候context2()函数的执行上下文会暂停并记录当前节点,然后创建了context1()函数的上下文,将其压入栈中,直至context1()运行完成,然后弹栈。之后恢复context2()的上下文,在记录的节点处继续执行直至也运行完成,弹栈。至于Global Context(全局执行上下文)是在JavaScript运行的时候就一直存在,一但它弹栈,就表明这不运行这段JavaScript代码了,比如关闭浏览器等。

变量对象(Variable Object)

变量对象是执行上下文的创建阶段创建的,它的创建经过了以下几个阶段:

  1. 创建并初始化arguments对象
  2. 建立属性名为函数名的属性,属性值为函数的内存地址
  3. 建立属性名为变量名的属性,属性值为undefined
 var a = 10;
 function fun(num) {
   return a + num;
 }
fun(3); // 13

上面这个例子中,变量对象创建的顺序为:

  1. 创建arguments对象,此时arguments里num的值为undefined
  2. 创建一个属性名为fun的属性,属性值为该函数在内存中的引用
  3. 创建一个属性名为a的属性,属性值为undefined

值得注意的是如果有两个相同函数名,则后面定义的会覆盖前面的值。但是如果变量名跟已经声明的形式参数、函数或者另一个变量名相同,则不会进行任何操作。

活动对象(Active Object)

变量对象和活动对象是同一个对象,只是他们处于执行上下文不同的生命周期

我们知道在变量对象中,所有的值只是为了初始化变量和函数,而活动对象则是为这些初始化的值赋予了实际的值。

接上面的例子,活动对象的值:

  1. 在arguments对象中,x此时为3
  2. 创建一个属性名为fun的属性,属性值为该函数在内存中的引用
  3. 创建一个属性名为a的属性,属性值为10

最后

想调试代码并查看上下文和this的可以亲自去这个链接测试一番。当然,你也可以先看看我的测试代码