执行环境
执行环境定义了变量和函数有权访问的其他数据,决定了他们各自的行为。
每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象上,虽然我们在编写代码的时候无法访问这个对象,但解析器在处理数据时会在后台用到它。
ECMAScript实现的宿主环境不同,表示的执行环境对象也不一样。
在javascript中,可执行的JavaScript代码分三种类型:
1. Global Code,即全局的、不在任何函数里面的代码,例如:一个js文件、嵌入在HTML页面中的js代码等。
2. Eval Code,即使用eval()函数动态执行的JS代码。
3. Function Code,即用户自定义函数中的函数体JS代码。
不同类型的JavaScript代码具有不同的执行环境,这里我们不考虑evel code,对应于global code和function code存在2种执行环境:全局执行环境和函数执行环境。
全局执行环境
在一个页面中,第一次载入JS代码时创建一个全局执行环境,全局执行环境是最外围的执行环境,在Web浏览器中,全局执行环境被认为是window对象。因此,所有的全局变量和函数都是作为window对象的属性和方法创建的。
某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁,全局执行环境直到应用程序退出后---例如关闭浏览器和网页---时才被销毁。
函数执行环境
每个函数都有自己的执行环境,当执行进入一个函数时,函数的执行环境就会被推入一个执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。这就是ECMAScript程序中的执行流。
【定义期】
函数定义的时候,都会创建一个[[scope]]属性,这个对象对应的是一个对象的列表,列表中的对象仅能javascript内部访问,没法通过语法访问。
我们定义一全局函数A,那么A函数就创建了一个A的[[scope]]属性。此时,[[scope]]里面只包含了全局对象【Global Object】。而如果, 我们在A的内部定义一个B函数,那B函数同样会创建一个[[scope]]属性,B的[[scope]]属性包含了两个对象,一个是A的活动对象【Activation Object】【对于函数来说】一个是全局对象,A的活动对象上面,全局对象在下面。以此类摧,每个函数的都在定义的时候创建自己的[[scope]],里面保存着一个类似于栈的格式的数据。
下面是示例代码:
// 外部函数 function A(){ var somevar; // 内部函数 function B(){ var somevar; } }
看下示意图:
【执行期】
当函数被执行的时候,就是进入这个函数的执行环境,首先会创一个它自己的活动对象【Activation Object】(这个对象中包含了this、参数(arguments)、局部变量(包括命名的参数)的定义,当然全局对象是没有arguments的)和一个变量对象的作用域链[[scope chain]],然后,把这个执行环境的[scope]按顺序复制到[[scope chain]]里,最后把这个活动对象推入到[[scope chain]]的顶部。这样[[scope chain]]就是一个有序的栈,这样保了对执行环境有权访问的所有变量和对象的有序访问。
// 第一步页面载入创全局执行环境global executing context和全局活动象 // 定义全局[[scope]],只含有Window对象 // 扫描全局的定义变量及函数对象:color【undefined】、changecolor【FD创建changecolor的[[scope]],此时里面只含有全局活动对象】,加入到window中,所以全局变量和全局函数对象都是做为window的属性定义的。 // FD已经定义好所以在此执行环境内任何位置都可以执行changecolor(),color也已经被定义,但是它的值是undefined // 第二步color赋值"blue" var color = "blue"; // 它是不需要赋值的,它就是引用本身 function changecolor() { // 第四步进入changecolor的执行环境 // 复制changecolor的[[scope]]到scope chain // 创建活动对象,扫描定义变量和定义函数,anothercolor【undefined】和swapcolors【FD创建swapcolors的[[scope]]加入changecolor的活动对象和全局活动对象】加入到活动对象,活动对象中同时还要加入arguments和this // 活动对象推入scope chain 顶端 // FD已经定义好所以在此执行环境内任何位置都可以执行swapcolors(),anothercolor也已经被定义好,但它的值是undefined // 第五anothercolor赋值"red" var anothercolor = "red"; // 它是不需要赋值的,它就是引用本身 function swapcolors() { // 第七步进入swapcolors的执行环境,创建它的活动对象 // 复制swapcolors的[[scope]]到scope chain // 扫描定义变量和定义函数对象,活动对象中加入变量tempcolor【undefined】以及arguments和this // 活动对象推入scope chain 顶端 // 第八步tempcolor赋值anothercolor,anothercolor和color会沿着scope chain被查到,并继续往下执行 var tempcolor = anothercolor; anothercolor = color; color = tempcolor; } // 第六步执行swapcolors,进入其执行环境 swapcolors(); } // 第三步执行changecolor,进入其执行环境 changecolor();
访问标识符:当执行js代码的过程中,遇到一个标识符,就会根据标识符的名称,在执行上下文(Execution Context)的作用域链中进行搜索。从作用域链的第一个对象(该函数的Activation Object对象)开始,如果没有找到,就搜索作用域链中的下一个对象,如此往复,直到找到了标识符的定义。如果在搜索完作用域中的最后一个对象,也就是全局对象(Global Object)以后也没有找到,则会抛出一个错误,提示undefined。
既然讲到执行期,顺便讲一下javascript的【声明提升机制Hoisting】
先看下面代码:
var myvar = 'my value'; alert(myvar); // my value
当然会弹出my value
再看下面这样
var myvar = 'my value'; (function(){ alert(myvar); // undefined var myvar = 'you value'; })()
结果却是undefined
这是因为,javascript解析器,进入一个函数执行环境,先对var 和 function进行扫描。相当于会把var或者function【函数声明】声明提升到执行环境顶部。
上面的代码会被解析成下面这样:
var myvar = 'my value'; (function(){ var myvar; alert(myvar); // undefined myvar = 'you value'; })()
根据,标识符找查机制当执行到alert的时候,查找myvar是局部变量的myvar,此时myvar并没有被赋值。所以结果是undefined。
再看看下面的例子:
count(1,2); // 3 function count(a,b) { alert(a+b); }
count(1,2); // 会报错误count is not a function var count = function (a,b) { alert(a+b); }
我们知道,根据声明提升机制,var和function都会被提升到执行环境的顶部,已经扫描完毕。所以,上面那种写法,会把function声明提到执行环境顶部,所以即使调用在声明的前面依然能够执行。
而下面这种写法会被解析成这样:
var count; count(1,2); // 会报错误count is not a function count = function (a,b) { alert(a+b); }
因为此时count只是被扫描,但还末被引用到函数对象,所以此时,它不是一个function,所以把它当函数来执行会报错。
另外一个需要提一下的是,函数的声明是优先于变量的声明的。
作用域
由上,可以得知,当查找一个个标识符的时候,是在作用域链[[scope chain]]里查找的,这个作用域链里只包含上一级的活动对象,但并不包括下一级的活动对象。这就意味着,内部执行环境可以访问外部的执行环境定义的变量,但外部的执行环境是无法访问内部执行环境定义的变量的。并且,内部环境定义的变量会覆盖掉外部环境定义的变量。我们还可以知道,特定执行环境的查找的标识符是在【定义期】就已经完成的,而不是在【执行期】,因为作用域链[[scope chain]]是从定义期的[scope]复制过来的。但是它本身的Activation Object对象是在执行期才推入,作用域链顶端的,所以本身的私有变量是【执行期】完成的。
还是上面的例子:
// color全局变量changecolor 和 swapcolors都可以沿着scope chain访问到 var color = "blue"; function changecolor() { // anothercolor局部变量,全局访问不到 但 changecolor和swapcolors都可以沿着scope chain访问到 var anothercolor = "red"; function swapcolors() { // tempcolo局部变量,全局和changecolor访问不到 但 swapcolors都可以沿着scope chain访问到 var tempcolor = anothercolor; anothercolor = color; color = tempcolor; } swapcolors(); } changecolor();
没有块级作用域
与C、C++以及JAVA不同,Javscript没有块级作用域。看下面代码:
if(true){ var myvar = "木乙"; } alert(myvar);// 木乙
根据上面我们讨论的,如果有块级作用域,外部是访问不到myvar的。再看下面
for (var i=0;i<5;i++){ console.log(i) } alert(i); // 5
对于有块级作用域的语言来说,i做为for初始化的变量,在for之外是访问不到的,这允分证明了,javascript是没有块级作用域的。
延长作用域链
虽然执行环境只有两种——全局作用域和函数作用域,但是还是可以通过某种方式来延长作用域链。因为有些语句可以在作用域链的顶部增加一个临时的变量对象。(function bildUrl(){ var qs = "?debug=true"; with(location){ var url = href + qs; } alert(url) })()
with会把location对象的所有属性和方法包含到变量对象中,并加入到作用域链的顶部。此时访问href实际上就是location.href。
但新声明的“url”变量会加入到最近的执行环境的变量对象里。试下面例子,说明url是可以访问到的,只是此时是undefined
(function bildUrl(){ var qs = "?debug=true"; if (!url) { alert("这里可以看到url"); //可以正常显示url已经被声明并且被扫描进函数的变量对象 } try{ if (!a) { alert(1);// 报错因为a根本就不存在 } } catch(e){ console.log("作用域链被延长了吧"); } with(location){ var url = href + qs; } })()
对catch语句来说,会创建一个新的变量对象加入到作用域链的顶部,其中包含的是被抛出的错误对象的声明。
需要说明的是,ie8之前的版本有个不符合标准的地方,就是被势出的错误对象会被加入到执行环境的变量对象。