执行环境(execution context) 中的 context
有着上下文和环境两层含义。
在JS中,对象是一个存储系统,是一系列键值对的集合,通过某个键也就是属性名,可以引用到它的值。这些值可能是普通的字符串或数字,也可能是对象。如果是函数对象,那么可称为该对象的方法。而JS中的函数是一个执行系统,函数可能是某个对象的属性,当它执行时,可以说这个_对象是这个函数的对象上下文_。函数的对象上下文是可以变化的,这取决于函数的对象上下文,也就是this
的值
当函数执行时,会进入一个特定的执行环境或叫做执行上下文,要明白执行上下文和对象上下文完全是不同的概念。执行上下文是在函数执行时创建的,每次进入函数都会创建一个完全不同的执行环境,也就是说执行环境是动态的。
在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
和其他命名参数的值来初始化**执行环境的变量对象
当Web页面中第一次载入JS代码时,会创建一个全局执行环境(window对象),所有全局变量和函数都作为window
对象的属性和方法而创建。
当Web页面中第一次载入JS代码时,会创建一个全局执行环境(window对象),所有全局变量和函数都作为window
对象的属性和方法而创建。
全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为**window
对象**,因此所有全局变量和函数都是作为window
对象的属性和方法创建。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出时才会被销毁,例如关闭网页或浏览器时。)
函数执行环境(局部执行环境)是函数执行执行过程中创建的。作用域链是基于执行环境的变量对象的,由所有执行环境的变量对象组成的。对于函数而言变量对象就是活动对象,因为在函数执行环境中,变量对象是不能直接访问的,此时由活动对象扮演变量对象的角色。当代码在一个环境中执行时,会创建变量的作用域链,作用域链的作用在于保证执行环境有权访问的变量和函数的有序访问。作用域的前端,始终都是_当前执行的代码_所在环境的变量对象。
当JS代码执行时,JS解释器会通过两个阶段产生一个执行环境:
- 1. 创建阶段:当函数被调用时,尚未执行函数内部代码之前。
- 创建变量对象
- 设置作用域属性的值
- 设置
this
的值
- 2. 激活阶段:代码执行阶段
- 初始化变量对象:设置变量的值、函数的引用
- 解释并执行代码
访问变量详细流程
创建变量对象
创建变量对象的流程如下
- 1. 根据函数的参数,创建并初始化
arguments
对象。 - 2. 声明提升
2.1. 扫描函数内部代码,查找函数声明。
查找所有函数声明将函数名和函数引用存入变量对象中,如果变量对象中已存在同名函数则覆盖。
2.2. 扫描函数内部代码查找变量声明
查找所有变量声明,将变量名存入变量对象中,初始化为undefined
。如果变量名和已声明的形参或函数同名,则什么也不做。
函数执行环境
每个函数都有自己的执行环境
定义期
函数定义时,会创建一个**[[Scope]]属性,[[Scope]]对象对应的是一个对象的列表**,列表中的对象仅供JS内部访问,无法通过语法访问。
执行期
当调用一个JS函数时,函数就会_进入_与该函数_相对应_的执行环境。如果又调用了另外一个函数,或者递归地调用同一个函数,则又会创建一个新的执行环境,在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因此,运行中的JS代码就构成了一个执行环境****栈。
简单来说,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。
环境栈:当JS代码执行的时候会进入不同的执行上下文,这些执行上下文会构成一个执行上下文栈(ECS, execution context stack)。
具体来说,当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其**活动对象(activation object)**作为变量对象。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境。全局执行环境的变量对象始终都是作用域链中的最后一个对象。
换句话说,程序在进入每个执行环境时,JS引擎在内部创建一个对象,叫做变量对象。对应到函数的每个参数时,在变量对象上会添加一个属性,属性名和值与参数名和值相同。函数中每声明一个局部变量,也会在变量对象上添加一个属性,属性名就是变量名,因此为变量赋值就是给变量对象属性赋值。在函数中访问参数或局部变量时,就是在变量对象上搜索对应的属性并返回其值。一般来说,变量对象是一个内部对象,JS代码中是无法直接访问的。ECMAScript规范中对其实现方式并不做要求,因此变量对象可能只是JS引擎内部的一种数据结构。
JS引擎将不同执行位置上的变量对象按照规则构建了一个链表,在访问一个变量时,先在链表的第一个变量对象上查找,如果没有找到则继续在第二个环境变量上查找,直到搜索结束。这就是作用域链的基本概念。
作用域链是一个变量对象的链表
访问标识符
当执行JS代码时遇到标识符,就会根据标识符的名称,在运行时上下文的_作用域链_中进行搜索。从作用域的第一个对象开始(如函数的活动对象)查找,如果没有找到,就搜索作用域链中下一个对象,如此往复直到找到标识符的定义。如果在搜索完作用域中的最后一个对象(全局对象)以后也没有找到,则会抛出一个错误,提示undefined
。
简单来说,标识符解析是沿着作用域链一级一级地搜索标识符的过程,搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止,如果找不到标识符,通常会导致错误发生。
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()的环境。 复制代码
函数参数被当做变量对象,其访问规则与执行环境的变量相同。