执行环境、变量对象和作用域链

执行环境又称执行上下文,英文缩写是EC(Execution Context),每当执行流转到可执行代码时,即会进入一个执行环境。在JavaScript中,执行环境分三种:

  • 全局执行环境 — 这个是最外围的代码执行环境,一旦代码被载入,引擎最先进入的就是这个环境。在浏览器中,全局环境就是window对象,一次所有全局属性和函数都是作为window对象的属性和方法创建的。全局执行环境直到应用程序退出时才会被销毁。
  • 函数执行环境 — 当执行一个函数时,JavaScript引擎进入执行环境。某个执行环境中的代码执行完之后,该环境销毁,保存在其中的所有变量和函数定义也随之销毁。
  • Eval执行环境 — Eval的执行环境和函数调用的执行环境相同。

活动的执行环境构成一个栈:栈的底部始终是全局环境,顶部是当前活动的执行环境。当执行流进入一个函数时,函数的环境被压入栈中。而在函数执行完之后,栈将其环境弹出,把控制权返回给之前的执行环境。

建立一个执行环境分为两个阶段:

  1. 进入上下文阶段:发生函数调用,进入执行环境时,此时具体的函数代码还没有执行。
  2. 执行代码阶段:进行变量赋值,函数引用,以及执行其它代码。

变量对象的英文缩写是VO(Variable Object),每一个执行环境都对应一个变量对象,这个对象存储着环境中定义的以下内容:

1. 函数的形参  

2. var声明的变量  

3. 函数声明(但不包含函数表达式)  

变量对象有两种存在方式,一种就是全局对象(用Global表示),存放着全局属性和函数,我们可以通过this关键字引用到该对象。另外一种是函数执行环境中定义的变量对象,改对象在函数的执行上下文中是不能直接访问的,被称为活动对象,英文缩写为AO(Activation Object)。

接下来我们来看下再不同的执行环境中,变量对象是怎样初始化的?

首先是全局环境中的变量对象,这个对象就是全局对象,全局对象是在进入任何执行环境之前就已经创建了的对象。这个对象只存在一份,它的属性在程序中的任何地方都可以访问,全局对象的生命周期终止于程序退出的那一刻。全局对象的初始化阶段,将Math、String等作为自身属性,初始化如下:

Global = {
   Math:{...},
   String:{...},
   ...
   ...
   window:Global // 引用自身
}; 

接下来我们重点研究下函数执行环境中的变量对象,即上文提到的活动对象。活动对象是在进入函数执行环境时创建的,它通过函数的arguments属性初始化:

AO = {
   arguments: {...} //参数对象,包括callee, length等属性
};  

理解了变量对象的初始化之后,接下来就是进入执行环境的代码部分了。上文中提到过,执行环境的建立分为两个阶段,第一个阶段就是进入上下文阶段。在该阶段,变量对象包含以下属性:

  1. 函数的所有形参:全局环境中没有形参,这里只是针对函数的执行环境而言。此时由形参名称和对应值构成变量对象的属性。如果没有传递相应的形参值,对应值为undefined。
  2. 所有的函数声明:需要注意的是这里特指函数的声明,函数表达式不算。此时有函数名和对应的函数对象构成变量对象的属性。如果变量对象已经存在同名的属性,则覆盖这个属性。
  3. 所有的变量声明:由var关键字声明的变量,由变量名和对应值组成,作为变量对象的属性。如果变量名与已经声明的形参或函数名相同,则变量声明不会干扰已经存在的这里属性。

上文中,我们提到过变量声明提前的问题,在这里就反映为在进入上下文阶段,首先将初始化变量声明,构成变量对象的属性,此时该属性的值为undefined。例如下面的例子:

function test(a, b){
   console.log(a); // 10
   console.log(b); // undefined
   console.log(c); // undefined
   console.log(d); // function d(){}
   console.log(e); // undefined
   console.log(f); //Reference error
   var c = 10;
   function d(){}
   var e = function _e(){};
   (function f(){});
}
test(10);  

我们考虑进入到带有参数10的test函数的执行环境时,在进入上下文阶段,活动对象初始化如下:

AO(test) = {
   a: 10,
   b: undefined,
   c: undefined,
   d: 指向函数d,
   e: undefined
};  

活动对象不包含属性f,这是因为f是一个函数表达式,而不是函数声明,函数表达式不会影响到变量对象。函数_e同样是函数表达式,但是它分配给了变量e,所以赋值语句执行后,就可以通过e访问到函数表达式_e。
接下来进入到执行环境的第二个阶段,执行代码。在这个阶段开始时,变量对象已经拥有了属性,参考上面的例子,代码执行后变量对象被修改为:

AO(test) = {
   a: 10,
   b: undefined,
   c: 10,
   d: 指向函数d,
   e: 指向函数表达式_e
}; 

理解了以上内容后,我们再来看一个例子:

function test2(a){
   console.log(a); // function a(){}
   var a = 3;
   console.log(a); // 3
   function a(){};
}
test2(20);  

上文中提到的在进入到执行上下文阶段时,变量对象会被初始化,在初始化阶段,变量声明构成的对象属性是最后被执行的,并且如果变量名和已经声明的函数名或形参同名的话,变量声明不会干扰到已经存在的属性,所以在函数执行环境的第一阶段,变量对象为:

AO(test2) = {
   a: 指向函数a
}; 

不过,在紧接着的代码执行阶段,属性a被重新赋值为3。

另外,需要特别指明的是变量只能通过var关键字来声明,对于类似于a=4这样的赋值语句,如果a没有通过var声明的话,相当于是创建了一个全局对象的属性,而并没有创建新的变量,它之所以可以认为是全局变量对象的属性,仅仅是因为全局对象等同于全局变量对象。参考以下代码:

function test3(){
   console.log(a); // undefined
   console.log(b); // Reference error
   var a = 3;
   b = 4;
}
test3();

所以在函数执行环境的第一阶段,变量对象为:

AO(test2) = {
   a: undefined
};  

因为b不是一个变量,所以在这个阶段,根本就不存在b,只有在代码执行阶段,b才会以全局对象的属性出现。但是还未执行到这里之前,就已经出错了。另外一个需要记住的就是,通过var声明的变量不能通过delete删除,而属性则可以,所以上述例子中的a是不可以通过delete删除的,而b则可以。

现在我们已经知道,执行环境中的数据作为属性存储在变量对象中,同时也知道,变量对象在在每次进入执行环境时创建,并初始化,在代码执行时,更新属性的值。接下来,将讨论下作用域链的概念。

作用域链大多数时候和内部函数有关,我们可以创建内部函数,甚至可以从父函数中返回这些函数。示例代码如下:

var x = 10;
function foo(){
   var y = 20;
   function bar(){
      console.log(x + y);
   }
   return bar;
}
foo(); // 30  

每个环境都有自己的变量对象,作用域链正是内部环境所有变量对象(包括父变量对象)的列表。此链用来在标识符解析中查找变量。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。对于上面的例子,bar执行环境中的作用域链包括:bar变量对象、foo变量对象和全局变量对象。

函数的作用域链是在函数调用时创建,包含这个函数的活动对象和[[scope]]属性。示例如下:

活动的执行环境 = {
   AO: 变量对象,
   this:thisValue,
   Scope: [变量对象列表] // 作用域链
};  

其中Scope = 被调用函数的活动对象 + 被调用函数的[[scope]]属性。

这种标识符的解析过程,与函数的生命周期有关。函数的生命周期可以分为创建和激活(调用时)两个阶段。在函数创建时,函数对象的内部存在一个[[scope]]属性,[[scope]]是所有父变量对象的层级链。[[scope]]属性在函数创建时被存储,永远不变,直到函数被销毁。函数可以不被调用,但该属性一直存在。与作用域链相比,作用域链是活动的执行环境的一个属性,而[[scope]]是函数的属性。

参考以上例子,foo函数在进入全局环境后被创建,此时foo函数拥有了[[scope]]属性,如下图所示:

同样的,bar函数在进入到foo函数的执行环境时被创建,此时foo函数的活动对象已经被创建,所以bar函数的[[scope]]属性如下图所示:

然后,在函数调用激活阶段,生成的活动对象和[[scope]]属性共同组成执行环境的作用域链。也就是说将活动对象添加到 [[scope]]链表的最前端,在查找标识符时,首先从自身变量对象开始,逐渐向父变量查找。

另外需要特别注意的是,通过构造函数创建的函数的[[scope]]属性中仅包含全局对象。

时间: 2024-11-08 16:02:40

执行环境、变量对象和作用域链的相关文章

变量对象、作用域链和This

变量对象 作用域链 This 整理自:https://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html 系列文章中变量对象,作用域链和this三篇文章 原文地址:https://www.cnblogs.com/shaunyang/p/10348640.html

javascript 执行环境,变量对象,作用域链

前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体. 通过上网查资料,特来总结,以备回顾和修正. 要讲的依次为: EC(执行环境或者执行上下文,Execution Context) ECS(执行环境栈Execution Context Stack) VO(变量对象,Variable Object)|AO(活动对象,Active Object) scope chain(作用域链)和[[scope]]属性 EC 每当控制器到达EC

使用变量对象引出作用域链

<script type="text/javascript"> var name="xm"; //全局变量,window.name===name; 返回:true function fn(){ //全局变--方法 , window.fn()====fn(); 返回:true var name="xh"; //局部变量 , 可以理解为fn().name===name.(本来是看不到的,为了好理解,虚拟为一个实例)  var sex=&q

*JS:执行环境、变量对象、活动对象和作用域链

var a=1; function b(x){ var c=2; console.log(x); } b(3); ·执行环境(execution context),也称为环境.执行上下文.上下文环境.执行上下文环境: 每次当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文.执行上下文(简称-EC)是ECMA-262标准里的一个抽象概念,用于同可执行代码(executable code)概念进行区分. 通俗的话来讲就是,JS中的函数运行不能仅仅看函数内部有哪些变量,再简单的

1--面试总结-js深入理解,对象,原型链,构造函数,执行上下文堆栈,执行上下文,变量对象,活动对象,作用域链,闭包,This

参考一手资料:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/中文翻译版本:https://zhuanlan.zhihu.com/p/32042645 Javascript 是一种单线程编程语言,这意味着它只有一个调用栈,call Stack(调用栈 ,,先入后出) 核心:对象,原型链,构造函数,执行上下文堆栈,执行上下文,变量对象,活动对象,作用域链,闭包,This js原型链? 定义 原型对象也是简单的对象并且可以拥有它们自

js内存空间 执行上下文 变量对象详解 作用域链与闭包 全方位解读this

内存空间:https://blog.csdn.net/pingfan592/article/details/55189622 执行上下文:https://blog.csdn.net/pingfan592/article/details/55189804 变量对象详解:https://blog.csdn.net/pingfan592/article/details/56009330 作用域链与闭包:https://blog.csdn.net/pingfan592/article/details/5

执行环境、作用域、作用域链、调用对象、闭包

执行环境 : 每调用一个函数时(执行函数时),系统会为该函数创建一个封闭的局部的运行环境,即该函数的执行环境.函数总是在自己的执行环境中执行,如读写局部变量.函数参数.运行内部逻辑.创建执行环境的过程包含了创建函数的作用域,函数也是在自己的作用域下执行的.从另一个角度说,每个函数执行环境都有一个作用域链,子函数的作用域链包括它的父函数的作用域链.关于作用域.作用域链请看下面. 作用域.作用域链.调用对象: 函数作用域分为词法作用域和动态作用域. 词法作用域是函数定义时的作用域,即静态作用域.当一

【JavaScript】4、执行环境、变量对象与声明提前

这段时间一直在看一些关于JavaScript的书,看到不明白的地方就满世界搜解答,结果今天晚上在搜索一个奇怪的语法的时候不小心点开一道面试题,是一道考察作用域的十年老题,于是我试着做了一下,果断被坑,结束后看解析,看得也不是很明白,于是赶紧回去看基础书,结果发现以前很多自己一眼扫过的知识点自己完全没有掌握,瞬间后悔万分,所以趁势赶紧重新学一下,同时把这些这些点记录下来. 原题是这样的: var tt = 'aa'; function test(){ alert(tt); var tt = 'dd

javascript 作用域链及闭包,AO,VO,执行环境

下面的文章内容会根据理解程度不断修正. js变量作用域: 定义:变量在它申明的函数体以及函数体内嵌套的任意函数体内有定义. function AA(){ var bb='我是AA内部变量'; function TT(){ alert(bb); } alert(bb); TT(); } AA(); 如上图,两次弹出的都是“我是AA内部变量”. JS的变量作用域是函数级的,也就是在AA内部申明的变量,在AA内部任意位置,包括它嵌套的函数内也是有定义的. 在函数AA外面,bb就是没有定义的.当然如果去