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

 壹 ? 引

我们都知道,JS代码的执行顺序总是与代码先后顺序有所差异,当先抛开异步问题你会发现就算是同步代码,它的执行也与你的预期不一致,比如:

function f1() {
    console.log(‘听风是风‘);
};
f1(); //echo

function f1() {
    console.log(‘echo‘);
};
f1(); //echo

按照代码书写顺序,应该先输出 听风是风,再输出 echo才对,很遗憾,两次输出均为 echo;如果我们将上述代码中的函数声明改为函数表达式,结果又不太一样:

var f1 = function () {
    console.log(‘听风是风‘);
};
f1(); //听风是风

var f1 = function() {
    console.log(‘echo‘);
};
f1(); //echo

这说明代码在执行前一定发生了某些微妙的变化,JS引擎究竟做了什么呢?这就不得不提JS执行上下文的了。

 贰 ? JS执行上下文

JS代码在执行前,JS引擎总要做一番准备工作,这份工作其实就是创建对应的执行上下文;

执行上下文有且只有三类,全局执行上下文,函数上下文,与eval上下文;由于eval一般不会使用,这里不做讨论。

1.全局执行上下文

全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是我们熟知的window对象,我们能通过this直接访问到它。

全局对象window上预定义了大量的方法和属性,我们在全局环境的任意处都能直接访问这些属性方法,同时window对象还是var声明的全局变量的载体。我们通过var创建的全局对象,都可以通过window直接访问。

2.函数执行上下文

函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文;需要注意的是,同一个函数被多次调用,都会创建一个新的上下文。

说到这你是否会想,上下文种类不同,而且创建的数量还这么多,它们之间的关系是怎么样的,又是谁来管理这些上下文呢,这就不得不说说执行上下文栈了。

 叁 ? 执行上下文栈(执行栈)

执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有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();
// 此时执行栈中只剩下一个全局执行上下文

那么到这里,我们解释了执行栈与执行上下文的存储规则;还记得我在前文提到代码执行前JS引擎会做准备创建执行上下文吗,具体怎么创建呢,我们接着说。

 肆 ? 执行上下文创建阶段

执行上下文创建分为创建阶段与执行阶段两个阶段,较为难理解应该是创建阶段,我们先说创建阶段。

JS执行上下文的创建阶段主要负责三件事:确定this---创建词法环境(LexicalEnvironment)---创建变量环境(VariableEnvironment)

这里我就直接借鉴了他人翻译资料的伪代码,来表示这个创建过程:

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

如果你有阅读其它关于执行上下文的文章读到这里一定有疑问,执行上下文创界过程不是应该解释this,作用域与变量对象/活动对象才对吗,怎么跟别的地方说的不一样,这点我后面解释。

1.确定this

官方的称呼为This Binding,在全局执行上下文中,this总是指向全局对象,例如浏览器环境下this指向window对象。

而在函数执行上下文中,this的值取决于函数的调用方式,如果被一个对象调用,那么this指向这个对象。否则this一般指向全局对象window或者undefined(严格模式)。

我在之前有专门写一篇介绍this的博文,现在看来写的很不好,之后我会重新理解写一篇通俗易懂的文章。

2.词法环境

词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用。

词法环境由环境记录与对外部环境引入记录两个部分组成。

其中环境记录用于存储变量和函数声明的实际位置,便于代码执行阶段能对应赋值。而外部环境引入记录用于保存它可以访问的其它外部环境,那么说到这个,是不是有点作用域链的意思?

我们在前文提到了全局执行上下文与函数执行上下文,所以这也导致了词法环境分为全局词法环境与函数词法环境两种。

全局词法环境:

对外部环境的引入记录为null,因为它本身就是最外层环境,除此之外它还包含了全局对象的所有属性方法,以及用户自定义的全局对象(通过var声明)。

函数词法环境:

包含了用户在函数中定义的所有变量外,还包含了一个arguments对象。函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境,这个根据实际代码而来。

这里借用译文中的伪代码(环境记录在全局和函数中也不同,全局中的环境记录叫对象环境记录,函数中环境记录叫声明性环境记录,说多了糊涂,下方有展示):

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

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: <GlobalLexicalEnvironment>
    },

    VariableEnvironment: {
      EnvironmentRecord: {
        Type: "Declarative",  // 声明性环境记录
        // 标识符绑定在这里  var创建的g在这
        g: undefined
      },
      // 外部环境引入记录为</Global>
      outer: <GlobalLexicalEnvironment>
    }
  }

不知道你有没有发现,在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数,而let  const被设置为未初始化。

现在你总知道变量提升与函数提升是怎么回事了吧,以及为什么let const为什么有暂时性死域,这是因为作用域创建阶段JS引擎对两者初始化赋值不同。

 伍 ? 关于变量对象与活动对象

回答前面的问题,为什么别人的博文介绍上下文都是谈作用域,变量对象和活动对象,我这就成了词法环境,变量环境了。

我在阅读相关资料也产生了这个疑问,一番查阅可以确定的是,变量对象与活动对象的概念是ES3提出的老概念,从ES5开始就用词法环境和变量环境替代了,因为更好解释。

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

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

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

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

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

我们来总结下上面提到的概念。

 陆 ? 总结

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

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

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

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

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

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

不得不说相关文章也是看的我心累,也希望对有缘的你有所帮助,那么到这里,本文结束。

 柒 ? 参考

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

【译】理解 Javascript 执行上下文和执行栈

JavaScript深入之执行上下文栈

JavaScript深入之变量对象

js中的活动对象与变量对象 什么区别?

原文地址:https://www.cnblogs.com/echolun/p/11438363.html

时间: 2024-11-05 18:42:44

一篇文章看懂JS执行上下文的相关文章

(好文推荐)一篇文章看懂JavaScript作用域链

闭包和作用域链是JavaScript中比较重要的概念,首先,看看几段简单的代码. 代码1: 1 var name = "stephenchan"; 2 var age = 23; 3 function myFunc() { 4 alert(name); 5 var name = "endlesscode"; 6 alert(name); 7 alert(age); 8 alert(weight); 9 } 10 myFunc(); 11 myFunc(); 上述代码

angularjs 一篇文章看懂自定义指令directive

 壹 ? 引 在angularjs开发中,指令的使用是无处无在的,我们习惯使用指令来拓展HTML:那么如何理解指令呢,你可以把它理解成在DOM元素上运行的函数,它可以帮助我们拓展DOM元素的功能.比如最常用ng-click可以让一个元素能监听click事件,这里你可能就有疑问了,同样都是监听为什么不直接使用click事件呢,angular提供的事件指令与传统指令有什么区别?我们来看一个例子: <body ng-controller="myCtrl as vm"> <d

一篇文章看懂Android学习最佳路线

为什么中高级Android程序员不多呢?这是一个问题,我不好回答,但是我想写一篇文章来描述下Android的学习路线,期望可以帮助更多的Android程序员提升自己. 作者:来源:Android开发中文站|2015-11-12 10:40 收藏 分享 前言 看到一篇文章中提到"最近几年国内的初级Android程序员已经很多了,但是中高级的Android技术人才仍然稀缺",这的确不假,从我在百度所进行的一些面试来看,找一个适合的高级Android工程师的确不容易,一般需要进行大量的面试才

一篇文章看懂spark 1.3+各版本特性

Spark 1.6.x的新特性Spark-1.6是Spark-2.0之前的最后一个版本.主要是三个大方面的改进:性能提升,新的 Dataset API 和数据科学功能的扩展.这是社区开发非常重要的一个里程碑.1. 性能提升根据 Apache Spark 官方 2015 年 Spark Survey,有 91% 的用户想要提升 Spark 的性能.Parquet 性能自动化内存管理流状态管理速度提升 10X 2. Dataset APISpark 团队引入了 DataFrames,新型Datase

小程序社交立减金正式开放!一篇文章看懂它

2018微信公开课PRO现场,微信小程序团队宣布,小程序立减金能力正式上线. 什么是社交立减金? 这是一款能够帮助商家快速获取社交.裂变传播属性的小程序经营工具--商家应用了这一能力后,用户通过支付.扫码等场景就能参与社交立减金的活动,将社交立减金礼包分享出去,就能获得一份立减金. 如何领取社交立减金? 1.用户在门店用微信支付消费后,可通过收到的模板消息领取立减金: 2.点击"邀请好友一起领取",让更多好友参与进来: 3.下次使用微信支付后,可以直接抵扣立减金. 社交立减金营销效果如

一篇文章看懂iOS代码块Block

iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,Block是一种特殊的数据类型,其可以正常定义变量.作为参数.作为返回值,特殊地,Block还可以保存一段代码,在需要的时候调用,目前Block已经广泛应用于iOS开发中,常用于GCD.动画.排序及各类回调 注: Block的声明与赋值只是保存了一段代码段,必须调用才能执行内部代码 Block变量的声明.赋值与调用 Block变量的声明 Block变量的声明格式为: 返回值类型(^Bl

一篇文章看懂Java并发和线程安全

一.前言 长久以来,一直想剖析一下Java线程安全的本质,但是苦于有些微观的点想不明白,便搁置了下来,前段时间慢慢想明白了,便把所有的点串联起来,趁着思路清晰,整理成这样一篇文章. 二.导读 1.为什么有多线程? 2.线程安全描述的本质问题是什么? 3.Java内存模型(JMM)数据可见性问题.指令重排序.内存屏障 三.揭晓答案 1.为什么有多线程 谈到多线程,我们很容易与高性能画上等号,但是并非如此,举个简单的例子,从1加到100,用四个线程计算不一定比一个线程来得快.因为线程的创建和上下文切

一篇文章看懂物体检测的发展脉络 转

转 https://zhuanlan.zhihu.com/p/28399320 第一,什么是物体检测,如何去评价一个物体里系统的好坏. 第二,物体检测整个的框架是怎么样的?它一般包含了图像的分类和物体检测的定位. 第三,介绍物体检测的历史发展,从传统的人工设计的一些图像特征加上分类器到现在的深度学习. ▼ What's Computer Vision 介绍物体检测之前,我们首先要知道什么是计算机视觉.计算机视觉是计算机科学的一个分支领域,旨在构造智能算法和程序,来"观察"这个世界.比如

微信小程序社交立减金正式开放!一篇文章看懂它(开通微信免充值代金券)

2018微信公开课PRO现场,微信小程序团队宣布,小程序立减金能力正式上线. 什么是社交立减金? 这是一款能够帮助商家快速获取社交.裂变传播属性的小程序经营工具--商家应用了这一能力后,用户通过支付.扫码等场景就能参与社交立减金的活动,将社交立减金礼包分享出去,就能获得一份立减金. 如何领取社交立减金? 1.用户在门店用微信支付消费后,可通过收到的模板消息领取立减金: 2.点击"邀请好友一起领取",让更多好友参与进来: 3.下次使用微信支付后,可以直接抵扣立减金. 社交立减金营销效果如