<li id="2aw4k"></li>
  • <div id="2aw4k"><tr id="2aw4k"></tr></div>
  • <div id="2aw4k"><tr id="2aw4k"></tr></div>
    <center id="2aw4k"><small id="2aw4k"></small></center><center id="2aw4k"><small id="2aw4k"></small></center>
    首页»JavaScript»解密JavaScript执行上下文

    解密JavaScript执行上下文

    来源:玩弄心里的鬼 发布时间:2019-05-22 阅读次数:

     执行上下文栈

      首先我们先了解一下什么是执行上下文栈(Execution context stack)。

      上面这张图来自于mdn,分别展示了栈、堆和队列,其中栈就是我们所说的执行上下文栈;堆是用于存储对象这?#25351;?#26434;类型,我们复制对象的地址引用就是这个堆内存的地址;队列就是异步队列,用于event loop的执行。

      JS代码在引擎中是以“一段一段”的方式来分析执行?#27169;?#32780;并非一行一行来分析执行。而这“一段一段”的可执行代码无非为三种:Global code、Function Code、Eval code。这些可执行代码在执行的时候又会创建一个一个的执行上下文(Execution context)。例如,当执行到一个函数的时候,JS引擎会做一些“准备工作”,而这个“准备工作”,我们称其为执行上下文。

      那么随着我们的执行上下文数量的增加,JS引擎又如何去管理这些执行上下文呢?这时便有了执行上下文栈。

      这里我用一段贯穿全文的例子来讲解执行上下文栈的执行过程:

    var scope = 'global scope';
    function checkscope(s) {
      var scope = 'local scope';
      function f() {
        return scope;
      }
      return f();
    }
    checkscope('scope');

      当JS引擎去解析代码的时候,最先碰到的就是Global code,所以一开始初始化的时候便会将全局上下文推入执行上下文栈,并且只有在整个应用程序执行完毕的时候,全局上下文才会推出执行上下文栈。

      这里我们用ECS来模拟执行上下文栈,用globalContext来表示全局上下文:

    ESC = [
       globalContext, // 一开始只有全局上下文
    ]
    

      然后当代码执行checkscope函数的时候,会创建checkscope函数的执行上下文,并将其压入执行上下文栈:

    ESC = [
      checkscopeContext, // checkscopeContext入栈
      globalContext,
    ]
    

      接着代码执行到return f()的时候,f函数的执行上下文被创建:

    ESC = [
      fContext, // fContext入栈
      checkscopeContext,
      globalContext,
    ]
    

      f函数执行完毕后,f函数的执行上下文出栈,随后checkscope函数执行完毕,checkscope函数的执行上下文出栈:

    // fContext出栈
    ESC = [
      // fContext出栈
      checkscopeContext,
      globalContext,
    ]
    // checkscopeContext出栈
    ESC = [
      // checkscopeContext出栈
      globalContext,
    ]
    

     变量对象

      每一个执行上下文?#21152;?#19977;个重要的属性:

    • 变量对象
    • 作用域链
    • this

      这一节我们先来说一下变量对象(Variable object,这里简称VO)。

      变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。并且不同的执行上下文也有着不同的变量对象,这里分为全局上下文中的变量对象和函数执行上下文中的变量对象。

      全局上下文中的变量对象

      全局上下文中的变量对象其实就是全局对象。我们可以通过this来访问全局对象,并?#20197;?#27983;览器环境中,this === window;在node环境中,this === global。

      函数上下文中的变量对象

      在函数上下文中的变量对象,我们用活动对象来表示(activation object,这里简称AO),为什么称其为活动对象呢,因为只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问。

      在函数执行之前,会为当前函数创建执行上下文,并?#20197;?#27492;时,会创建变量对象:

    • 根据函数arguments属性初始化arguments对象;
    • 根据函数声明生成对应的属性,其值为一个指向内存中函数的引用指针。如果函数名称已存在,则覆盖;
    • 根据变?#21487;?#26126;生成对应的属性,此时初始值为undefined。如果变量名已声明,则忽略该变?#21487;?#26126;;

      还是以刚才的代码为例:

    var scope = 'global scope';
    function checkscope(s) {
      var scope = 'local scope';
      function f() {
        return scope;
      }
      return f();
    }
    checkscope('scope');
    

      在执行checkscope函数之前,会为其创建执行上下文,并初始化变量对象,此时的变量对象为:

    VO = {
      arguments: {
        0: 'scope',
        length: 1,
      },
      s: 'scope', // 传入的参数
      f: pointer to function f(),
      scope: undefined, // 此时声明的变量为undefined
    }
    

      随着checkscope函数的执行,变量对象被激活,变相对象内的属?#36816;?#30528;代码的执行而改变:

    VO = {
      arguments: {
        0: 'scope',
        length: 1,
      },
      s: 'scope', // 传入的参数
      f: pointer to function f(),
      scope: 'local scope', // 变量赋值
    }
    

      其实也可以用另一个概念“函数提升”和“变量提升”来解释:

    function checkscope(s) {
      function f() { // 函数提升
        return scope;
      }
      var scope; // 变?#21487;?#26126;提升	
      scope = 'local scope' // 变量对象的激活也相当于此时的变量赋值
      return f();
    }
    

     作用域链

      每一个执行上下文?#21152;?#19977;个重要的属性:

    • 变量对象
    • 作用域链
    • this

      这一节我们说一下作用域链。

      什么是作用域链

      当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有?#19994;劍?#23601;会从父级执行上下文的变量对象中查找,一?#38381;业?#20840;局上下文的变量对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

      下面还是用我们的例子来讲解作用域链:

    var scope = 'global scope';	
    function checkscope(s) {
      var scope = 'local scope';	
      function f() {
        return scope;
      }
      return f();
    }
    checkscope('scope');

      首先在checkscope函数声明的时候,内部会绑定一个[[scope]]的内部属性:

    checkscope.[[scope]] = [
      globalContext.VO
    ];
    

      接着在checkscope函数执行之前,创建执行上下文checkscopeContext,并推入执行上下文栈:

    • 复制函数的[[scope]]属性初始化作用域链;
    • 创建变量对象;
    • 将变量对象压入作用域链的最顶端;
       
    // -> 初始化作用域链;
    checkscopeContext = {
      scope: checkscope.[[scope]],
    }
    
    // -> 创建变量对象
    checkscopeContext = {
      scope: checkscope.[[scope]],
      VO = {
        arguments: {
          0: 'scope',
          length: 1,
        },
        s: 'scope', // 传入的参数
        f: pointer to function f(),
        scope: undefined, // 此时声明的变量为undefined
      },
    }
    
    // -> 将变量对象压入作用域链的最顶端
    checkscopeContext = {
      scope: [VO, checkscope.[[scope]]],
      VO = {
        arguments: {
        0: 'scope',
        length: 1,
      },
      s: 'scope', // 传入的参数
      f: pointer to function f(),
      scope: undefined, // 此时声明的变量为undefined
      },
    }
    

      接着,随着函数的执行,修改变量对象:

    checkscopeContext = {
      scope: [VO, checkscope.[[scope]]],
      VO = {
        arguments: {
          0: 'scope',
          length: 1,
        },
        s: 'scope', // 传入的参数 
        f: pointer to function f(),
        scope: 'local scope', // 变量赋值
      }
    }
    

      与此同时遇到f函数声明,f函数绑定[[scope]]属性:

    checkscope.[[scope]] = [
      checkscopeContext.VO, // f函数的作用域还包括checkscope的变量对象
      globalContext.VO
    ];
    

      之后f函数的步骤同checkscope函数。

      再来一个经典的例子:

    var data = [];	
    for (var i = 0; i < 6; i++) {
      data[i] = function () {
        console.log(i); 
      }; 
    }
    data[0]();
    // ...
    

      很简单,?#36824;?#35775;问data几,最终console打印出来的都是6,因为在ES6之前,JS都没有块级作用域的概念,for循环内的代码都在全局作用域下。

      在data函数执行之前,此时全局上下文的变量对象为:

    globalContext.VO = { 
      data: [pointer to function ()], 
      i: 6, // 注意:此时的i值为6 
    }
    

      每一个data匿名函数的执行上下文?#21019;?#33268;都如下:

    data[n]Context = {
      scope: [VO, globalContext.VO],
      VO: {
        arguments: {
          length: 0,
        }
      }
    }
    

      那么在函数执行的时候,会先去自己匿名函数的变量对象上找i的值,发现没有后会沿着作用域链查找,?#19994;?#20102;全局执行上下文的变量对象,而此时全局执行上下文的变量对象中的i为6,所以每一次都打印的是6了。

      词法作用域 & 动态作用域

      JavaScript这门语言是基于词法作用域?#21019;?#24314;作用域?#27169;?#20063;就是说一个函数的作用域在函数声明的时候就已经确定了,而不是函数执行的时候。

      改一下之前的例子:

    var scope = 'global scope';
    function f() {
      console.log(scope)
    }
    function checkscope() {
      var scope = 'local scope';
      f();
    }
    checkscope();
    

      因为JavaScript是基于词法作用域创建作用域?#27169;?#25152;以打印的结果是global scope而不是local scope。我们结合上面的作用域链来分析一下:

      首先遇到了f函数的声明,此时为其绑定[[scope]]属性:

    // 这里就是我们所说的“一个函数的作用域在函数声明的时候就已经确定了”
    f.[[scope]] = [
      globalContext.VO, // 此时的全局上下文的变量对象中保存着scope = 'global scope';
    ];

      然后我们直接跳过checkscope的执行上下文的创建和执行的过程,直接来到f函数的执行上。此时在函数执行之前初始化f函数的执行上下文:

    // 这里就是为什么会打印global scope
    fContext = {
      scope: [VO, globalContext.VO], // 复制f.[[scope]],f.[[scope]]只有全局执行上下文的变量对象
      VO = {
        arguments: {
          length: 0,
        },
      },
    }

      然后到了f函数执行的过程,console.log(scope),会沿着f函数的作用域链查找scope变量,先是去自己执行上下文的变量对象中查找,没有?#19994;劍?#28982;后去global执行上下文的变量对象上查找,此时scope的值为global scope。

     this

      在这里this绑定也可以分为全局执行上下文和函数执行上下文:

    • 在全局执行上下文中,this的指向全局对象。(在浏览器中,this引用 Window 对象)。
    • 在函数执行上下文中,this 的?#31561;?#20915;于该函数是如何被调用的。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的?#24403;?#35774;置为全局对象或者undefined(在严格模式下)

      总结起来就是,谁调用了,this就指向谁。

     执行上下文

      这里,根据之前的例子来完整的走一遍执行上下文的流程:

    var scope = 'global scope';
    function checkscope(s) {
      var scope = 'local scope';
      function f() {
        return scope;
      }
      return f();
    }
    checkscope('scope');

      首先,执行全?#25191;?#30721;,创建全局执行上下文,并且全局执行上下文进入执行上下文栈:

    globalContext = {
      scope: [globalContext.VO],
      VO: global,
      this: globalContext.VO
    }
    ESC = [
      globalContext,
    ]

      然后随着代码的执行,走到了checkscope函数声明的阶段,此时绑定[[scope]]属性:

    checkscope.[[scope]] = [
      globalContext.VO,
    ]

      在checkscope函数执行之前,创建checkscope函数的执行上下文,并且checkscope执行上下文入栈:

    // 创建执行上下文
    checkscopeContext = {
      scope: [VO, globalContext.VO], // 复制[[scope]]属性,然后VO推入作用域链顶端
      VO = {
        arguments: {
          0: 'scope',
          length: 1,
        },
        s: 'scope', // 传入的参数
        f: pointer to function f(),
        scope: undefined,
      },
      this: globalContext.VO,
    }
    
    // 进入执行上下文栈
    ESC = [
      checkscopeContext,
      globalContext,
    ]

      checkscope函数执行,更新变量对象:

    // 创建执行上下文
    checkscopeContext = {
      scope: [VO, globalContext.VO], // 复制[[scope]]属性,然后VO推入作用域链顶端
      VO = {
        arguments: {
          0: 'scope',
          length: 1,
        },
        s: 'scope', // 传入的参数
        f: pointer to function f(),
        scope: 'local scope', // 更新变量
      },
      this: globalContext.VO,
    }

      f函数声明,绑定[[scope]]属性:

    f.[[scope]] = [
      checkscopeContext.VO,
      globalContext.VO,
    ]

      f函数执行,创建执行上下文,推入执行上下文栈:

    // 创建执行上下文
    fContext = {
      scope: [VO, checkscopeContext.VO, globalContext.VO], // 复制[[scope]]属性,然后VO推入作用域链顶端
      VO = {
        arguments: {
          length: 0,
        },
      },
      this: globalContext.VO,
    }
    	
    // 入栈
    ESC = [
      fContext,
      checkscopeContext,
      globalContext,
    ]
    

      f函数执行完成,f函数执行上下文出栈,checkscope函数执行完成,checkscope函数出栈:

    ESC = [
      // fContext出栈
      checkscopeContext,
      globalContext,
    ]
    
    ESC = [
      // checkscopeContext出栈,
      globalContext,
    ]
    

      到此,一个整体的执行上下文的流程就分析完了。

    QQ群:WEB开发者官方群(515171538),验证消息:10000
    微信群:加小编微信 849023636 邀请您加入,验证消息:10000
    提示:更多精彩内容关注微信公众号:全栈开发者?#34892;模╢sder-com)
    网友评论(共0条评论) 正在载入评论......
    理智评论文明上网,拒绝恶意谩骂 发表评论 / 共0条评论
    登录会员?#34892;?/span>
    大乐透彩票预测
    <li id="2aw4k"></li>
  • <div id="2aw4k"><tr id="2aw4k"></tr></div>
  • <div id="2aw4k"><tr id="2aw4k"></tr></div>
    <center id="2aw4k"><small id="2aw4k"></small></center><center id="2aw4k"><small id="2aw4k"></small></center>
    <li id="2aw4k"></li>
  • <div id="2aw4k"><tr id="2aw4k"></tr></div>
  • <div id="2aw4k"><tr id="2aw4k"></tr></div>
    <center id="2aw4k"><small id="2aw4k"></small></center><center id="2aw4k"><small id="2aw4k"></small></center>
    查今日湖北刂选5走势图 宝马线上娱乐mg 篮球规则走步视频 德州扑克和梭哈的区别 海南环岛赛上的各种表情 湖北快三一定牛-百度 上海天天彩选4第2018348期 球赛分析 意甲转会 pk10为何前赢后输 3d今天所有开机号 江苏快三今天的预测号 福彩 排列三跨度振幅走势图 特区南国彩票论坛七星彩论坛