浅谈JavaScript执行上下文与作用域,作用域链

1,199 阅读9分钟

       JavaScript中执行上下文,变量对象,作用域与作用域链等是一系列基础性的且十分重要的概念。他们之间有着千丝万缕的联系,深入理解这些概念以及他们之间的联系有助于我们理解在js开发中遇到一些问题,例如:

  • es6之前,使用var声明的变量为什么会存在变量提升,过程是什么样的,es6为什么新增let和const?
  • 块级作用域解决了什么问题?
  • this指向是什么时候确定,怎么确定的?

一.执行上下文

       执行上下文,通俗的讲就是当前js代码的执行环境。如何确定当前代码的执行环境?这里则要提到另一个概念,执行环境栈。 Javascript引擎在执行代码之前, 首先会创建一个执行环境栈. 然后创建全局执行环境并将它压入栈中作为栈底。之后每遇到一个函数执行时,都会为该函数创建执行上下文,并将其推入执行环境栈中。JavaScript中的运行环境主要是全局环境和局部环境(函数)。栈遵循先入后出的原则,因此处于栈顶的执行环境将优先执行。栈底永远是全局环境,当浏览器窗口关闭,全局环境才会出栈。当调用一个函数时,一个新的执行上下文就会被创建。一个执行上下文的生命周期可以分为如下几个阶段:

  • 1 创建阶段        在这个阶段中,执行上下文会分别创建变量对象,确定作用域链,确定this指向(即this指向是在函数被调用时才被确定)。

  • 2 代码执行阶段        完成变量赋值,以及执行其他代码。

  • 3 销毁阶段        函数执行完毕后, 执行环境栈便会把这个函数的执行环境弹出, 并将控制权返回给之前的执行环境。上面提到 执行上下文有3个重要属性:

1.变量对象(VO)
2.作用域链(Scope chain)
3.this

下面做逐一分析。

二.变量对象。

       每个执行环境都有一个与之相关联的对象,执行环境中声明的变量和函数都在其中,不能直接访问该对象,这个对象就是变量对象(VO)。由此我们便知道VO中存储了在执行环境中定义的变量和函数声明.通常情况下,一个VO对象有以下信息:

  • 变量
  • 函数
  • 形参

变量对象的创建会经历以下几个过程。

1、建立arguments对象:检查当前上下文中的参数,建立该对象下的属性与属性值。
2、检查当前上下文的函数声明,在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的指针。
3、检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined(变量提升),const/let 声明的变量没有赋值,不能提前使用,在当前阶段(执行上下文创建)若 var 声明的变量与函数同名,以函数值为准。

       需要注意的是,未进入执行阶段之前,变量对象中的属性都不能访问。但是进入执行阶段之后,变量对象转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。 下面用一段伪代码来描述一下变量对象的存在形式。

function foo(){
    function bar (){
        console.log(name)
    }
    bar()

    var name = '小明'
    
}
foo()
/* foo调用时,其执行上下文被创建
foo.EC = {
    VO:{ //变量对象
        this:window, //独立调用,非严格模式下指向window(浏览器环境)
        arguments:[],函数参数
        bar: function 
        name: undefined //var声明的变量 将被提升并赋值为undefined
    },
    [[scope chain]]:{}  // 作用域链 下一章节做详细说明
} 
 */

       需要注意一点,let和const声明的变量,在被变量对象收集后不会被赋值为undefined,因此不能在其声明前调用。const、let会在声明的地方到块级顶部形成暂时性死区,在这区间使用该变量都会报错。另外,全局执行上下文比较特殊,它的变量对象,就是window对象(浏览器环境)。this也是指向window。

三.作用域与作用域链。

1.作用域

       通俗的讲即当前执行上下文中的变量和函数的可访问(可查找)范围。它可以理解为一种约束或一套规则,定义在当前作用域中的变量和函数在外部一般是不可访问的,例如我们在函数中声明的变量一般情况下在函数外是无法访问的(可以通过特殊手段访问,后面会介绍)。

       在es6之前,作用域有两种,即全局作用域和局部作用域(函数作用域)。es6加入了块级作用域,什么是块?任何一对花括号中的语句集都属于一个块,在这其中定义的所有变量在块外都是不可见的,我们称之为块级作用域。由于es6之前没有块级作用域,因此块中声明的变量在块外仍然是可以访问的,这就会造成一些问题例如:

  • 变量提升导致局部作用域的变量覆盖全局变量。
 var name = '小明'
 function foo(){
   
   console.log(name) // undefined   var定义的变量,没有块的概念,因此if代码块中的name可以被外部访问。被提升并被赋值为undefined
   
   if (true)
   {
     var name = '小红'
   }
   
 }
 foo()

而有了块级作用域,则不会有这个问题。可以使用let或const声明块级变量来解决上述问题。

 var name = '小明'
 function foo(){
   
   console.log(name) // 小明
   
   if (true)
   {
     const name = '小红'
   }
   
 }
 foo()

2.循环中的计数变量泄露成全局变量

for (var i = 0; i < 5; i++) {
    console.log(i)
}
console.log(i) // 5i=5时循环结束,此时外部可以访问到i

 
for (let i = 0; i < 5; i++) {
    console.log(i)
}
console.log(i) // i is not defined 块级变量外部无法访问

由此可见有了块级作用域,之前用立即执行函数生成私有作用域的做法已经不再需要了。

2.作用域链

       一般情况下当前执行上下文的变量取值会到当前上下文的变量对象中取值。 但是如果在当前变量对象中没有查到值,就会向上层环境中变量对象的去查,直到查到全局作用域,这个查找过程形成的链条就叫做作用域链。由此可见作用域链是由当前环境与上层环境的一系列变量对象组成的。它和执行上下文有关,用于在处理标识符(变量名或函数名)的时候进行变量查询。作用域链在函数调用的时候创建出来, 它包含了活动对象(变量对象在执行阶段变为活动对象)和该函数的内部**[[Scope]]**属性。函数的Scope 属性是函数定义的时候创建的,这个属性对应的是一个对象列表。该列表中存储着与之相关联作用域的变量对象,该对象仅能js内部访问。因此,如果用一个数组scopeList来模拟作用域链,则scopeList[0]即代表当前上下文的活动对象,其余元素[scope]属性中的所有对象的顺序排列。该数组的最末端是全局变量对象。 下面用一段伪代码描述作用域链的存在形式。

var a = 1

function foo() {
    var b = 2
    console.log(a)

    function bar() {
        var c = 3
        console.log(b)
    }
    bar()
}
foo()
/* 
 js默认进入全局执行环境 
 foo的定义阶段

 foo.[[scope]] = { 该属性保存了与该函数相关的变量对象,显然
                   foo的上层环境是window,window变量对象没有arguments属性
    GO:{
        this:window,
        window:{...},
        a:undefined
    }
 }
 
 foo进入执行阶段  bar函数才会被定义
 此时由于foo函数已经执行,因此其变量对象已经被创建

 bar.[[scope]] = {
     AO(foo):{
         this:window, 函数被调用时确定this指向,显然foo为独立调用,非严格模式下this指向window
         arguments:[],
         b:2
     },
     GO:{
        this:window,
        window:{...},
        document:{...},
        a:1
    }
 }


*/


// 执行阶段


// foo函数调用时

/* 

foo.EC = { //foo函数的执行上下文
    AO:{
        this:window,
        arguments:[],
        b:2,
        bar:function
    },
    [[scope chain]]:{ foo的作用域链
        AO:AO, // 推入作用域链顶部的活动对象
        GO:{....} // [[scope]]中的全局对象
    } 
}

bar.EC = {
    AO:{
        this:window,
        arguments:[],
        c:3
    },
    [[scope chain]]:{
        AO(bar):AO,
        AO(foo):{
         this:window,
         arguments:[],
         b:2
        },
        GO:{
            this:window,
            window:{...},
            document:{...},
            a:1
        }
    }
}
*/

       简单总结一下,函数定义的时,会创建一个[[scope]]属性 。这个属性对应的是与当前函数相关执行环境的变量对象列表。 函数调用时,当前函数的执行上下文被创建,创建变量对象,确定作用域链 ,确定this指向。 变量对象主要保存当前上下文的参数,变量以及函数。作用域链顶部是当前上下文的活动对象,其余元素是scope属性的对象列表,最底层是全局变量对象。而this指向则主要看是谁调用的该函数(或者说函数调用时被哪个对象所拥有)。

参考:

www.bilibili.com/video/BV1hE… www.jianshu.com/p/330b1505e…