*javascript引擎内部在执行代码以前到底做了些什么?为什么某些函数以及变量在没有被声明以前就可以被使用?以及它们的最终的值是怎样被定义的?
伴随着这些问题,再深入学习一下Javascript的执行环境、作用域,以备后续的闭包学习。
一、执行上下文
在Javascript中有三种代码执行环境:
- Global Code
全局级别的代码。这个是最默认的代码执行环境,是最外围的一个执行环境(在web浏览器中,全局执行环境被认为是window对象)。一旦代码被载入,引擎最先进入的是这个环境。
- Function Code
函数级别的代码。当执行一个函数时,运行函数体中的代码。
- Eval Code
在Eval函数中运行的代码
先看一个栗子:
var color = "blue"; function changeColor(){ var anotherColor = "red"; function swapColors(){ var tempColor = anotherColor; anotherColor = color; color = tempColor; } swapColors(); } changeColor(); alert("color is now:"+color); //red alert("tempColor:"+tempColor); //wrong
在这个例子中,总共有三个执行环境:全局环境、changeColor()的局部环境、swapColors()的局部环境。为什么在swapColors()函数中,可以访问幷改变变量color的值?最后在全局环境中却不能访问swapColors()函数中定义的变量tempColor?
这又得学习执行上下文堆栈。
二、执行上下文堆栈
在浏览器中,javascript引擎的工作方式是单线程的。也就是说,某一时刻只有唯一的一个事件是被激活处理的,其它的事件被放入队列中,等待被处理。而每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
三、建立阶段和代码执行阶段
每当调用一个函数时,一个新的执行上下文就会被创建出来。然而,在javascript引擎内部,这个上下文的创建过程具体分为两个阶段:(1)建立阶段;(2)执行阶段
Javascript引擎在每个阶段都做什么工作呢?
先看几个例子:
alert(message);
var message;
结果:
恩??不是应该报错吗?可是却没有,Javascript引擎在执行alert函数时,貌似已经知道message已经被声明了,那如果在后边给初始化一下呢,在执行alert函数时,会不会知道这个message变量在后边被初始化了呢?
测试下:
alert(message);
var message = 10;
结果:
恩??看来Javascript引擎在执行alert函数前就已经声明了变量message,所以顺序是:声明变量message->执行alert()->初始化变量message。
再看个例子:
console.log(fun);
function fun(a,b){
return a+b;
}
结果:
由此可知,在Javascript引擎执行console.log()方法时就已经知道fun是一个方法,而且还知道方法里的具体操作。
那再看:
console.log(f);
var f = function fun(a,b){
return a+b;
}
结果:
在我们定义一个函数有几种方式,其中函数声明和函数表达式这两种情况在Javascript引擎工作时却有着差别。后者的f会被先申明。
根据查看的资料可知:Javascript引擎在建立阶段主要做三件事:
- 建立变量,函数,arguments对象,参数
- 建立作用域链(后续还得继续学习)
- 确定this的值(后续还得继续学习)
实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性:
executionContextObj = { variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ }, scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ }, this: {} }
而在代码执行阶段主要工作就是变量赋值,函数引用以及执行其它代码。
四、执行流程(http://blogread.cn/it/article/6178)
确切地说,执行上下文对象(上述的executionContextObj)是在函数被调用时,但是在函数体被真正执行以前所创建的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 - 建立阶段。这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,然后基于这些信息建立执行上下文对象(executionContextObj)。在这个阶段,variableObject对象,作用域链,以及this所指向的对象都会被确定。
上述第一个阶段的具体过程如下:
-
找到当前上下文中的调用函数的代码
-
在执行被调用的函数体中的代码以前,开始创建执行上下文
-
进入第一个阶段-建立阶段:
-
建立variableObject对象:
-
建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值
-
检查当前上下文中的函数声明:
-
每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用
-
如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。
-
-
检查当前上下文中的变量声明:
-
每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined。
-
如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。
-
-
-
初始化作用域链
-
确定上下文中this的指向对象
-
-
代码执行阶段:
-
执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。
-
通过例子看下:
var a =10, fn, bar = function(x){ var b = 5; fn(x+b); } fu = function(y){ var c = 5; console.log(y+c); } bar(10);
在执行代码之前,首先会创建全局的上下文环境。
全局上下文环境 | |
a | undefined |
fn | undefined |
bar | undefined |
this | window |
然后是代码执行。在代码执行bar(10);之前,上下文的变量都被赋值。
全局上下文环境 | |
a | 10 |
fn | function |
bar | function |
this | window |
然后就是执行bar(10);。在执行函数体语句之前,Javascript引擎先会进入准备阶段。创建一个新的执行上下文环境。
bar函数的上下文环境 | |
b | undefined |
x | 10 |
arguments |
{ 0:10, length:1 } |
this | window |
scope | {...} |
执行开始,走到语句var b = 5;时,函数上下文变化为;
bar函数的上下文环境 | |
b | 5 |
x | 10 |
arguments |
{ 0:10, length:1 } |
this | window |
scope | {...} |
然后又走到fn(x+b);时调用函数fn,此时又会创建fn函数的上下文。
fn函数的上下文环境 | |
c | undefined |
y | 15 |
arguments |
{ 0:15, length:1 } |
this | window |
scope | {...} |
然后开始执行fn函数的代码,fn函数的上下文变为:
fn函数的上下文环境 | |
c | 5 |
y | 15 |
arguments |
{ 0:15, length:1 } |
this | window |
scope | {...} |
fn函数的代码执行完后,fn函数的上下文就会被销毁,然后继续执行bar函数的代码,执行完bar函数的代码后,销毁bar函数的上下文。
整个上下文的堆栈过程如图所示:
这下,我们就知道为什么在调用之后声明都不会出错了~
推荐:
http://www.cnblogs.com/wangfupeng1988/
http://web.jobbole.com/84044/