JS相关让前端飞Web前端之路

js深度剖析 : 执行环境和作用域链

2017-09-02  本文已影响84人  小貔

执行上下文

execution context 又称执行上下文或者执行环境.

执行上下文是JavaScript中一个极为重要的概念. 我们可以简单的理解执行上下文就是我们代码执行时所在的环境. 这个环境中包含着许多对我们来说十分重要的数据. 例如作用域链 , 数据的访问权,执行代码等等 .

当我们的代码在JavaScript中运行时,便会进入不同的执行上下文.

其中Global execution context是最外围层的一个执行环境 , 即JavaScript代码开始运行的默认环境.也就是我们常说的全局环境. 这个执行上下文是由浏览器创建并初始化的 .

除此之外,每个函数都有自己的一个执行上下文. 当我们进行函数调用时, 其执行上下文便会创建. 这也是我们所认为的局部环境( local context) .

variable object

对于全局环境,浏览器中用window对象来作为它的variable object . 该全局对象中存储着JavaScript所定义的全部内置对象和全局函数,以及宿主环境的宿主对象和我们在全局环境中定义的函数和变量等.

这也是为什么我们在全局环境中声明一个变量和函数后,其会成为window对象的属性.

var a = 11;
function fn(){};

window.a ===a ; // -> true 
window.fn ===fn; // -> true 

对于局部环境,我们通常用activation object来作为variable object .并且这个对象我们是不能用代码访问到的.这个对象中存储着我们在函数内部声明的变量和函数以及参数(包含隐式参数arguments).

注: 下文会将其简写为AO

我们还要说明一下,activation object在最开始的时候只有一个变量,即arguments对象(这个对象在全局环境上是不存在的).

执行上下文的两个阶段

常言道: 兵马未动 粮草先行

因此当我们进入一个执行上下文的时候,js解释器并不会立即就去执行其内部代码, 而是先做一些预备工作来保证之后的执行无后顾之忧.

其大致可分为两个阶段:

然后下文中我们会着重讲一下准备阶段中一些过程的细节.

AO(activation object)的创建过程

  1. 在创建activation object之初,就为其添加了一个名为arguments对象,里面存储着函数调用时传入的所有参数.

  2. 扫描函数的形式参数

    • 对于所有找到的参数,将由参数名和其对应值以key-value的形式存入VO/AO中,如果没有传递对应参数的话,那么将参数名和undefined值以key-value的形式存入VO/AO中.
  3. 扫描函数内部代码,查找函数声明(Function declaration).

    • 对于所有找到的函数声明,将函数名和函数引用存入VO/AO中.
    • 并且如果VO/AO中已经有同名的标识符, 那么就对其标识符的引用进行覆盖.
  4. 扫描函数内部代码,查找变量声明(Variable declaration)

    • 对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为"undefined".
    • 如果变量名称跟已经声明的参数或函数名相同,则变量声明不会干扰已经存在的标识符和其引用.

我们以如下代码为例:

function add(a,b){
    var z ;
    function fn(){
        console.log(a,b)
    }
    z = a+b;
    return z;
}

add(10,20)

接着我们来分析一下AO的创建过程, 如图:

创建AO过程创建AO过程

然后我们来验证一下当标识符命名发生冲突时会怎样:

    function fn(x){
        function x(){};
        console.log(x)
    }
    
    fn(10) // -> ƒ x(){}

我们会发现当参数名和函数名冲突时,函数的引用值会覆盖参数名的引用值.

   function fn(x){
            var x ;
            console.log(x)
        }
        
    fn(10) // -> 10

我们会发现当参数名和变量名冲突时,变量声明不会干扰已经存在的标识符和其引用.

   function fn(){
             function x(){};
             var x ;
            console.log(x)
        }
        
    fn() // ->  ƒ x(){}

这一结果又刚好进一步证明了我们上一步得出结论.

我们再来看最后一段代码:

function fn(){
    console.log(x)
    var x = 10 ;
}
fn() // -> undefined

我们看见了,其输出的结果是undefined. 不知你有没有发现,上述代码与变量提升有关.而AO的创建过程则正是造成变量提升这一现象的根源.

函数对象的[[scope]]属性

在JavaScript中, 一切皆为对象

JavaScript 中每个函数都表示为一个函数对象(函数实例),既然是对象,就有相关的属性和方法。除了正常的属性,函数对象具有仅供 JavaScript 引擎内部使用,但不能通过代码访问的一系列内部属性。这些属性中,其中一个就是 [[scope]] 属性。

函数内部的 [[scope]]属性实际是一个variable object的集合.其包含了当前执行环境栈中所有执行上下文variable object. 而这也正是作用链的雏形.

注意: 因为该函数还未调用,所以执行环境栈中没有这个函数的执行上下文.

作用域链

正如上文所述, 执行上下文中有很多对我们来说十分重要的数据. 除了variable object以外 ,作用域链也是其中之一.

  1. 将函数内部[scope]属性中的所有对象按顺序复制到作用域链中,完成作用域链的初始化.
  2. 将当前执行上下文variable object放入作用域链的顶端,彻底完成作用域链的创建.

正因此,当前执行上下文的variable object一直都是其作用域链的开头 .

当在某个环境中为了读取或是写入而引用一个标志符,必须通过搜索来确定标识符代表什么. 搜索过程从作用域链的顶端开始,向上逐级查询与给定名字匹配的标识符.

在此我们再详细说明一下其搜索过程的细节. 如果在局部环境中找到了该标识符,搜索就会停止,变量就绪. 若是没有找到该变量名/函数名 ,那么就会沿着作用域链查找.搜索过程会一种追溯到全局对象上,若是还没有查找到该标识符,那么便意味着该变量/函数并未声明,输出错误信息.

var a = 10 ;
var b = 100;
function fn(){
     var b = 20 ;
     console.log(a) // -> 10 
     console.log(b) // -> 20
}
fn()

以上段代码为例,其在沿着作用域链上的查询过程如图:

作用域链作用域链

结束语

写这篇文章的原因来自一次偶然的思考, 然后便是大量的翻阅相关资料.甚至为此我还用我那有些蹩脚的英文翻墙去查阅一些外文资料.其过程自然有些艰辛的,但是黄天不负有心人,我总算是理解了JavaScript中的一些核心概念.并且我能感受的到,一扇新的大门即将为我开启, 征程还未停止.不过我有些喜欢上了这种在路上的感觉了.

我在路上我在路上
上一篇下一篇

猜你喜欢

热点阅读