Javascript浅谈之执行上下文
简介
每次当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文。执行上下文(简称-EC)是ECMA-262标准里的一个抽象概念,用于同可执行代码(executable code)概念进行区分。
概念区分(可执行代码、执行上下文、作用域)
1.可行执行代码(executable code),顾名思义就是可以用来执行的代码。可执行代码类型有三:
(1)全局代码
全局代码之的是当前页面中的所有代码,如:外部引入的js文件和<script>标签内的js代码,但是不包括函数内部的代码。
(2)函数代码
函数代码就是指所有函数内部的代码。
(3)Eval代码
通过eval函数执行的代码。
可执行代码是从代码的角度来描述这个页面中所有的js代码。
2.执行上下文(execution contexts, EC)
执行上下文是指代码执行时候的执行环境。它除了包含有当前的上下文代码,又还有一个执行上下文对象。这个对象中含有:变量对象、this、作用域链。
ExecutionContext = { VO: {...}, // 变量对象 this: thisValue, //当前执行环境中的this Scope: [ // Scope chain // 所有变量对象的列表 // for identifiers lookup ] };
执行上下文和可执行代码的关系相当密切,因为可执行代码只是一个静态的描述,而执行上下文则是对可执行代码的一个动态描述,也就是说,如果代码不执行则已,此时可以说执行上下文不存在也可以说执行上下文就是可执行代码,但是一旦代码执行,就会生成一个执行上下文,每进入一个可执行代码中,就生成一个新的执行上下文,而这些执行上下文在逻辑上又组成了一个执行上下文栈(ECStack)。
3.作用域(Scope)
在讲述作用域之前,先跟谈一下块级作用域这个概念。块级作用域是之“{...}”内部的代码。在块级作用域中声明的变量外部是不可见的。
for(int i=0, len = list.size; i<len; i++) { String str = (String)list.get(i); } System.out.println(str); //报错,变量不存在
不同于java、C、C++等,在ECMAScript中,不存在块级作用域。
var arr = [1,2,3]; for(var len = arr.length; len>0; len--){ var str = arr[len]; } console.log("len:",len," str:", str); //输出 len: 0 str: 2
那为什么js中不存在块级作用域?根据ECMAScipt中规定,每个可执行代码块中只能有一个执行上下文,而执行上下文中有个变量对象来管理在可执行代码块中出现的所有变量和函数声明,所有对于上面变量len、str和变量arr由同一个变量对象来管理,这样对于for而言就根本不可能产生什么块级作用域,声明出内部变量。看到这肯定有人要说了,你就会找软柿子捏,有本事你用函数试试。我对此只是呵呵一笑,你难道忘了前面我们已经说过了,函数代码可单独是一类可执行代码哦。
前面说了这么多,其实还没有说到作用域的本质,无论什么东西,存在必定有其理由,作用域的主体是什么(也就是作用域是对什么而言的)?我们为什么需要作用域?
作用域是针对变量来说的,标明变量可见的范围。(当然要实现这个还得通过作用域链这个机制才能实现,后面我会专门分析一下作用域链这个东东。)
同上面对比,细心的同学就会发现,执行上下文跟作用域根本不是一个概念,执行上下文中是包括作用域链的,不过初学者还是可有可能弄混,故在此作下对比分析。
执行上下文栈
活动的执行上下文组在逻辑上组成一个堆栈。堆栈底部永远都是全局上下文(global context),而顶部就是当前(活动的)执行上下文。堆栈在EC类型进入和退出上下文的时候被修改(推入或弹出)。
当进入funtion函数代码(所有类型的funtions)的时候,ECStack被压入新元素。如下所示,我们使函数自己调自己的方式递归一次:
(function foo(bar) { if (bar) { return; } foo(true); })();
那么,ECStack以如下方式被改变:
// 第一次foo的激活调用 ECStack = [ <foo> functionContext globalContext ]; // foo的递归激活调用 ECStack = [ <foo> functionContext – recursively <foo> functionContext globalContext ];
每次return的时候,都会退出当前执行上下文的,相应地ECStack就会弹出,栈指针会自动移动位置,这是一个典型的堆栈实现方式。一个抛出的异常如果没被截获的话也有可能从一个或多个执行上下文退出。相关代码执行完以后,ECStack只会包含全局上下文(global context),一直到整个应用程序结束。
总结
javascript中的执行环境是动态变化的,这点很重要。这一节对理解后面的变量对象、作用域链等是一个理论的基础。还有一点,我上面对eval的执行上下文没有进行分析,这个可不怪我,eval这玩意我平时很少用,而且各种规范都限制或者抵制用,导致这个了解比较少,后面有机会专门写一章关于eval。
文章中部分参考汤姆大叔博客,在此对大叔表示感激。