js执行上下文

在这篇文章中,我将深入探讨JavaScript中一个最基本的部分,即Execution Context。 在本文结束时,您应该更清楚地知道解释器是怎么工作的,为什么某些函数/变量在声明之前就可以使用以及它们的值是如何确定的。

1、什么是执行上下文

当Javascript代码运行的时候,所处在当前运行时的环境,就是执行上下文

通俗一点讲就是当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,就叫做“执行上下文(execution context)”

2、执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文(Global):代码首次执行时候的默认环境,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文(Function):每当一个函数被调用时,都会为该函数创建一个新的上下文,每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • eval执行上下文(Eval):当eval函数内部的文本执行的时候

你可以拥有任意数量的函数上下文。每一次函数调用都会创建一个新的上下文,它会创建一个私有域,函数内部做出的所有声明都会放在这个私有域中,并且这些声明在当前函数作用域外无法直接访问。

// global context
var sayHello = 'hello';

function person() { // execution context
  var first = 'xiaoming',
      last = 'xiaoli';

  function firstName() { // execution context
    return first;
  }

  function lastName() { // execution context
    return last;
  }

  console.log(`${sayHello} ${firstName()} ${lastName()}`);
}

3、执行栈

执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。

JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。

function f1() {
    f2();
    console.log(1);
};

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

function f3() {
    console.log(3);
};

f1(); // 3 2 1

/*
* 通过执行栈与上下文的关系来解释上述代码的执行过程,为了方便理解,
* 我们假设执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈,
* 因此过程大致如下:
*/

//代码执行前创建全局执行上下文
ECStack = [globalContext];
// f1调用
ECStack.push('f1 functionContext');
// f1又调用了f2,f2执行完毕之前无法console 1
ECStack.push('f2 functionContext');
// f2又调用了f3,f3执行完毕之前无法console 2
ECStack.push('f3 functionContext');
// f3执行完毕,输出3并出栈
ECStack.pop();
// f2执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文

关于执行栈,有5点需要记住:

  • 单线程
  • 同步执行
  • 一个全局上下文
  • 无数的函数上下文
  • 每次函数调用都会创建一个新的执行上下文,即使是调用自身

4、执行上下文创建阶段

执行上下文创建有两个阶段:(1)创建阶段(2)执行阶段

4.1 创建阶段

js执行上下文的创建阶段主要负责三件事:

  1. this 值的决定,即我们所熟知的 This 绑定
  2. 创建词法环境组件(LexicalEnvironment)。
  3. 创建变量环境组件(VariableEnvironment)。

所以执行上下文在概念上表示如下:

ExecutionContext = {  
    // 确定this的值
    ThisBinding = <this value>,
    // 创建词法环境组件
    LexicalEnvironment = {},
    // 创建变量环境组件
    VariableEnvironment = {},
};

4.1.1 This 绑定:

在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。

在函数执行上下文中,this的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefined(在严格模式下)。

js五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解

4.1.2 词法环境

官方es6 文档把词法定义为:

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

在词法环境的内部有两个组件:(1)环境记录器(2)一个外部环境的引用

  1. 环境记录器是存储变量和函数声明的实际位置。
  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

词法环境有两种类型:全局词法环境函数词法环境

  • 全局词法环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this 的值指向全局对象。
  • 函数词法环境在函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型:

  1. 全局环境中,环境记录器是对象环境记录器,用来定义出现在全局上下文中的变量和函数的关系。
  2. 函数环境中,环境记录器是声明式环境记录器,用来存储变量、函数和参数。

对于函数环境-声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length

词法环境在伪代码中看起来像这样:

// 全局环境
GlobalExectionContext = {
    // 全局词法环境
    LexicalEnvironment: {
        // 环境记录
        EnvironmentRecord: {
            Type: "Object", //类型为对象环境记录
            // 标识符绑定在这里 
        },
        outer: < null >
    }
};
// 函数环境
FunctionExectionContext = {
    // 函数词法环境
    LexicalEnvironment: {
        // 环境纪录
        EnvironmentRecord: {
            Type: "Declarative", //类型为声明性环境记录
            // 标识符绑定在这里 
        },
        outer: < Global or outerfunction environment reference >
    }
};

4.1.3 变量环境

变量环境同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系,它具备词法环境所有属性。

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

我们通过一串伪代码来理解它们:

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

// 我们用伪代码来描述上述代码中执行上下文的创建过程:

//全局执行上下文
GlobalExectionContext = {
    // this绑定为全局对象
    ThisBinding: <Global Object>,
    // 词法环境
    LexicalEnvironment: {  
        //环境记录
      EnvironmentRecord: {  
        Type: "Object",  // 对象环境记录
        // 标识符绑定在这里 let const创建的变量a b在这
        a: < uninitialized >,  
        b: < uninitialized >,  
        multiply: < func >  
      }
      // 全局环境外部环境引入为null
      outer: <null>  
    },
    // 变量环境
    VariableEnvironment: {  
      EnvironmentRecord: {  
        Type: "Object",  // 对象环境记录
        // 标识符绑定在这里  var创建的c在这
        c: undefined,  
      }
      // 全局环境外部环境引入为null
      outer: <null>  
    }  
  }

  // 函数执行上下文
  FunctionExectionContext = {
     //由于函数是默认调用 this绑定同样是全局对象
    ThisBinding: <Global Object>,
    // 词法环境
    LexicalEnvironment: {  
      EnvironmentRecord: {  
        Type: "Declarative",  // 声明性环境记录
        // 标识符绑定在这里  arguments对象在这
        Arguments: {0: 20, 1: 30, length: 2},  
      },  
      // 外部环境引入记录为</Global>
      outer: <GlobalEnvironment>  
    },
    // 变量环境
    VariableEnvironment: {  
      EnvironmentRecord: {  
        Type: "Declarative",  // 声明性环境记录
        // 标识符绑定在这里  var创建的g在这
        g: undefined  
      },  
      // 外部环境引入记录为</Global>
      outer: <GlobalEnvironment>  
    }  
  }

可能你已经注意到,在创建阶段,letconst 定义的变量没有任何关联的值,但 var 定义的变量被设置为 undefined

这是因为,在创建阶段,代码会扫描变量和函数声明,而函数声明则完整地存储在环境中,变量最初设置为 undefined (在情况下 var )或保持未初始化(在情况下)letconst )。

这就是为什么你可以 var 在声明之前访问已定义的变量(尽管 undefined )但在声明之前访问let和const变量时会出现引用错误。

这就是我们所说的提升。

4.2 执行阶段

在此阶段,完成对所有这些变量的分配,最后执行代码。

在执行阶段,如果 JavaScript 引擎 let 在源代码中声明的实际位置找不到变量的值,那么它将分配给它的值 undefined

5、关于变量对象与活动对象

变量对象与活动对象的概念是 ES3 提出的老概念,从 ES5 开始就用词法环境和变量环境替代

在上文中,我们通过介绍词法环境与变量环境解释了为什么var会存在变量提升,为什么let const没有,而通过变量对象与活动对象是很难解释的,由其是在JavaScript在更新中不断在弥补当初设计的缺陷。

其次,词法环境的概念与变量对象这类概念也是可以对应上的。

我们知道变量对象与活动对象其实都是变量对象,变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

那这不正好对应到了全局词法记录与函数词法记录了吗。而且由于ES6新增的let const不存在变量提升,于是正好有了词法环境与变量环境的概念来解释这个问题。

所以说到这,你也不用为词法环境,变量对象的概念闹冲突了。

6、总结

  1. 全局执行上下文一般由浏览器创建,代码执行时就会创建;函数执行上下文只有函数被调用时才会创建,调用多少次函数就会创建多少上下文。

  2. 调用栈用于存放所有执行上下文,满足 FILO 规则。

  3. 执行上下文创建阶段分为绑定this,创建词法环境,变量环境三步,两者区别在于词法环境存放函数声明与 const let 声明的变量,而变量环境只存储var声明的变量。

  4. 词法环境主要由环境记录与外部环境引入记录两个部分组成,全局上下文与函数上下文的外部环境引入记录不一样,全局为 null,函数为全局环境或者其它函数环境。环境记录也不一样,全局叫对象环境记录,函数叫声明性环境记录。

  5. 你应该明白了为什么会存在变量提升,函数提升,而 let const 没有。

  6. ES3 之前的变量对象与活动对象的概念在ES5之后由词法环境,变量环境来解释,两者概念不冲突,后者理解更为通俗易懂。

所以我们已经讨论了 JavaScript 程序是如何在内部执行的。虽然你不需要学习所有这些概念才能成为一个了不起的 JavaScript 开发人员,但对上述概念有一个很好的理解将帮助你更容易、更深入地理解其他概念,如提升、作用域和闭包。

7、参考

[译] 理解 JavaScript 中的执行上下文和执行栈

一篇文章看懂JS执行上下文

JavaScript深入之变量对象

JavaScript深入之执行上下文栈

理解JavaScript 中的执行上下文和执行栈

JS中的执行上下文(Execution Context)和栈(stack)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354