JS执行环境

执行环境(execution context,运行期上下文)是JS中最为重要的一个概念

执行环境(execution context) 中的 context 有着上下文和环境两层含义,对比另外一个概念“对象上下文”,对象上下文在面向对象的语言非常普遍,因为每段代码都在某个对象内部运行的。比如说Java,由于Java的函数不具有独立性,需要依附于某个特定的对象而存在,因此又叫方法。在JS中这个概念非常让人迷惑,特别是和执行环境联系起来的时候。

其实在JS中,对象是一个存储系统,是一系列键值对的集合,通过某个键也就是属性名,可以引用到它的值。这些值可能是普通的字符串或数字,也可能是对象。如果是函数对象,那么可称为该对象的方法。而JS中的函数是一个执行系统,函数可能是某个对象的属性,当它执行时,可以说这个对象是这个函数的对象上下文。函数的对象上下文是可以变化的,这取决于函数的对象上下文,也就是this的值。

4933701-ed2ad69f43b3ae31.png
函数与对象

但是当函数执行时,会进入一个特定的执行环境或叫做执行上下文,要明白执行上下文和对象上下文完全是不同的概念。执行上下文是在函数执行时创建的,每次进入函数都会创建一个完全不同的执行环境,也就是说执行环境是动态的。

在JS中,可执行的JS代码分为三种类型,它们分别对应不同类型的执行环境。

  • Global Code:全局的、不在任何函数内的代码
  • Eval Code:使用eval()函数动态执行的JS代码
  • Function Code:用户自定义函数中函数体内JS代码

不同类型的JS代码具有不同的执行环境,所有的JS代码都是在一个执行环境中被执行的。执行环境是一种概念,同时也是一种机制。用来完成JS运行时在作用域、生存期等方面的处理。执行环境定义了变量或函数是否具有访问其他数据的权限,进而决定各自行为。

其中全局环境只有一个,且一直存在,而函数环境是进入函数时创建的。JS实现为单线程,因此同一时刻仅有一个环境处于运行状态。因此,可以把JS的执行实现为栈,每次进入一个新的执行环境,都会把该环境置于栈顶执行,执行完毕后可弹出。因此形成了一个执行上下文栈。而变量的查找正是基于该栈的。

每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然编写的代码无法访问到这个对象,但解析器在处理数据时会在后台使用它。

执行环境组成

当JS代码执行时,就会进入不同的执行环境,每个执行环境由三部分构成:

  • 变量对象(VO, variable object):变量对象即包含变量的对象,开发者无法直接访问。
  • 作用域属性:[[Scope]]属性时一个指向单向链表的头结点的指针
    作用域即变量对象,作用域链是一个由变量对象组成的带头结点的单向链表,作用是用来进行变量查找。
  • this:指向一个环境对象

首先要明白,执行环境和作用域是完全不同的概念。从根本上来讲,作用域是基于函数的,而执行环境是基于对象的。换句话说,作用域涉及到被调用函数中的变量访问,而且不同调用场景是不一样的。执行环境始终是this关键字的值,它是拥有当前所执行代码的对象的引用。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然编码中无法访问这个对象,但解析器在处理数据时会在后台使用它。

执行环境分类

  • 全局执行环境
    在浏览器环境中全局执行环境就是window对象,window对象是JS代码开始运行时的默认环境。全局执行环境的变量对象始终都是作用域中的最后一个对象。
  • 函数执行环境
    当某函数被调用时,首先会创建一个执行环境及相应的作用域链,然后使用arguments和其他命名参数的值来初始化执行环境的变量对象。
4933701-e3d2bee870b13a8d.png
image.png

当Web页面中第一次载入JS代码时,会创建一个全局执行环境(window对象),所有全局变量和函数都作为window对象的属性和方法而创建。

4933701-fe7149a0ace48e00.png
执行环境分类

全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出时才会被销毁,例如关闭网页或浏览器时。)。

函数执行环境(局部执行环境)是函数执行执行过程中创建的。作用域链是基于执行环境的变量对象的,由所有执行环境的变量对象组成的。对于函数而言变量对象就是活动对象,因为在函数执行环境中,变量对象是不能直接访问的,此时由活动对象扮演变量对象的角色。当代码在一个环境中执行时,会创建变量的作用域链,作用域链的作用在于保证执行环境有权访问的变量和函数的有序访问。作用域的前端,始终都是当前执行的代码所在环境的变量对象。

执行环境的生命周期

4933701-aa48b0bdab808dc8.png
执行环境的生命周期

当JS代码执行时,JS解释器会通过两个阶段产生一个执行环境:

  1. 创建阶段:当函数被调用时,尚未执行函数内部代码之前。
  • 创建变量对象
  • 设置作用域属性的值
  • 设置this的值
  1. 激活阶段:代码执行阶段
  • 初始化变量对象:设置变量的值、函数的引用
  • 解释并执行代码

创建变量对象

创建变量对象的流程如下

  1. 根据函数的参数,创建并初始化arguments对象。
  2. 声明提升
    2.1. 扫描函数内部代码,查找函数声明。
    查找所有函数声明将函数名和函数引用存入变量对象中,如果变量对象中已存在同名函数则覆盖。
    2.2. 扫描函数内部代码查找变量声明
    查找所有变量声明,将变量名存入变量对象中,初始化为undefined。如果变量名和已声明的形参或函数同名,则什么也不做。

函数执行环境

每个函数都有自己的执行环境

定义期

函数定义时,会创建一个[[Scope]]属性,[[Scope]]对象对应的是一个对象的列表,列表中的对象仅供JS内部访问,无法通过语法访问。

执行期

当调用一个JS函数时,函数就会进入与该函数相对应的执行环境。如果又调用了另外一个函数,或者递归地调用同一个函数,则又会创建一个新的执行环境,在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因此,运行中的JS代码就构成了一个执行环境栈。

4933701-3ebbb29998e30f0c.png
执行流

简单来说,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。

环境栈:当JS代码执行的时候会进入不同的执行上下文,这些执行上下文会构成一个执行上下文栈(ECS, execution context stack)

4933701-9366b35ca6b38332.png
ECS

具体来说,当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境。全局执行环境的变量对象始终都是作用域链中的最后一个对象。

换句话说,程序在进入每个执行环境时,JS引擎在内部创建一个对象,叫做变量对象。对应到函数的每个参数时,在变量对象上会添加一个属性,属性名和值与参数名和值相同。函数中每声明一个局部变量,也会在变量对象上添加一个属性,属性名就是变量名,因此为变量赋值就是给变量对象属性赋值。在函数中访问参数或局部变量时,就是在变量对象上搜索对应的属性并返回其值。一般来说,变量对象是一个内部对象,JS代码中是无法直接访问的。ECMAScript规范中对其实现方式并不做要求,因此变量对象可能只是JS引擎内部的一种数据结构。

JS引擎将不同执行位置上的变量对象按照规则构建了一个链表,在访问一个变量时,先在链表的第一个变量对象上查找,如果没有找到则继续在第二个环境变量上查找,直到搜索结束。这就是作用域链的基本概念。

作用域链是一个变量对象的链表

访问标识符

当执行JS代码时遇到标识符,就会根据标识符的名称,在运行时上下文的作用域链中进行搜索。从作用域的第一个对象开始(如函数的活动对象)查找,如果没有找到,就搜索作用域链中下一个对象,如此往复直到找到标识符的定义。如果在搜索完作用域中的最后一个对象(全局对象)以后也没有找到,则会抛出一个错误,提示undefined

4933701-49c76023b2203341.png
访问标识符

简单来说,标识符解析是沿着作用域链一级一级地搜索标识符的过程,搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止,如果找不到标识符,通常会导致错误发生。

4933701-e5ded7e89e56bd89.png
eg
var color = 'red';
// 函数的作用域包含了两个对象:
// 1. 函数自身的变量对象,其中定义着arguments对象。
// 2. 全局环境的变量对象
function changeColor(){
  // 函数内部可以访问变量color,因为在作用域链中找到它。
  if(color==='red'){
    color = 'green';
  }else{
    color = 'blue';
  }
}

changeColor();
console.log(color);// green

在局部作用域中定义的变量可在局部环境与全局变量互换使用

var color = 'red';
function changeColor(){
    var localColor = 'green';
    function swapColor(){
        var tmpColor = localColor;
        localColor = color;
        color = tmpColor;
    }
    swapColor();
}
changeColor();
console.log(color);// green

代码涉及3个运行环境:
1. 全局环境
    |- 变量color
    |- 函数changeColor()
2. changeColor()局部环境:可访问全局环境中的变量
    |- 变量localColor
    |- 函数swapColor()
3. swapColor() 局部环境
    |- 变量 tmpColor:当前环境中才能访问,其他环境无权访问。

作用域链结构
window 全局环境
  |- color
  |- changeColor() 局部环境
    |- localColor
    |- swapColor() 局部环境
      |- tmpColor

swapColor()内部可访问其他两个环境中的所有变量,因为它们是它的父执行环境。

对于swapColor()而言,其作用域链上包含3个对象:
|- swapColor()的变量对象
|- changeColor()的变量对象
|- 全局变量对象

swapColor()的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则搜索上一级作用域。
changeColor()的作用域链中只包含两个对象:自己的变量对象和全局对象,所以它不能访问swapColor()的环境。
4933701-e60a5d0056b38437.png
changeColor

内部环境可通过作用域链访问所有的外部环境,外部环境不能访问内部环境中的任何变量和函数。这些环境之间联系是线性的、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名。但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。

函数参数被当做变量对象,其访问规则与执行环境的变量相同。

声明提升机制

https://www.cnblogs.com/pigtail/archive/2012/07/19/2570988.html

延长作用域链

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值