【转】Javascript中的函数和执行环境

转自:http://zhangbo-peipei-163-com.iteye.com/blog/1773959

函数是Javascript的主要组建部分,函数定义了诸如闭包、“this”关键字、全局变量、局部变量等诸多的特性。理解函数是真正理解Javascript工作机制的第一步。 

一、ExecutionContext的创建 
总所周知,函数能够访问声明在当前函数作用域“之外”的变量、全局变量、声明在函数内部的变量以及通过参数传进来的变量和指向“容器对象”的"this"变量。以上所有这些变量为我们的函数形成了一个“环境”,该“环境”定义了哪些变量和它们的值是可以被当前函数访问的。一部分“环境”是随着函数的定义而定义的,其他一些是函数访问的时候才定义的。 
当一个函数被访问时,一个ExecutionContext 被创建,ExecutionContext定义了函数“环境”大部分,接下来看一下ExecutionContext是怎么构建的(注意顺序): 

利用伪代码演示例子: 

Js代码   收藏代码
  1. function foo (a, b, c)   
  2. {  
  3.     function z(){alert(‘Z!’);}  
  4.     var d = 3;  
  5. }  
  6. foo(‘foo’,’bar’);  


1.    arguments属性被创建,arguments属性是一个类似数组的对象,该对象的整数类型的属性分别引用传递给函数的参数值,顺序和传参时顺序一致。arguments对象包含length(参数个数)和callee(引用被调用的函数本身)属性。 

Js代码   收藏代码
  1. ExecutionContext:   
  2. {  
  3.     arguments: {  
  4.         0: ‘foo’, 1: ‘bar’,  
  5.         length: 2, callee: function() //Points to foo function  
  6.     }  
  7. }  


2.    函数域被创建,[[scope]]属性和我们上面所说的ExecutionContext,更多的细节后面会讲到。 

3.    变量实例化,分为3个子步骤(也是有顺序的): 
3.1    ExecutionContext会为每一个定义在函数签名中的参数定义一个属性,如果在前面已经创建的arguments对象中对应的位置有一个值,这个值被分配给该属性,否则,该属性值为undefined。 

Js代码   收藏代码
  1. ExecutionContext: {  
  2.     arguments: {  
  3.         0: ‘foo’, 1: ‘bar’,  
  4.         length: 2, callee: function() //Points to foo function  
  5.     },  
  6.     a: ‘foo’, b: ‘bar’, c: undefined  
  7. }  


3.2    扫描函数体检测其中声明的函数————FunctionDeclarations,这些声明的函数被创建并且作为属性分配给ExecutionContext,属性名就是该函数名称。 

Js代码   收藏代码
  1. ExecutionContext: {  
  2.     arguments: {  
  3.         0: ‘foo’, 1: ‘bar’,  
  4.         length: 2, callee: function() //Points to foo function   
  5.     },  
  6.     a: ‘foo’, b: ‘bar’, c: undefined,  
  7.     z: function() //Created z() function  
  8. }  


3.3    扫描函数体检测其中声明的变量,这些变量作为ExecutionContext的属性保存在ExecutionContext中并且被初始化成undefined。 

Js代码   收藏代码
  1. ExecutionContext: {  
  2.     arguments: {  
  3.         0: ‘foo’, 1: ‘bar’,  
  4.         length: 2, callee: function() //Points to foo function  
  5.     },  
  6.     a: ‘foo’, b: ‘bar’, c: undefined,  
  7.     z: function(), //Created z() function,  
  8.     d: undefined  
  9. }  


4.    “this”属性被创建,它的值依赖于函数的访问方式。 
a.    正常函数(myFunction(1,2,3))。“this”指向全局对象(i.e. window)。 
b.    对象方法(myObject.myFunction(1,2,3))。“this”指向包含该函数的对象,例中的myObject对象。 
c.    类似于setTimeout() 或者 setInterval()的回调函数,“this”指向全局对象(i.e. window)。 
d.    call()或者apply()函数,“this”指向call()/apply()函数的第一个参数。 
e.    作为构造函数(new myFunction(1,2,3))。“this”是一个以myFunction.prototype作为原型的空对象。 

Js代码   收藏代码
  1. ExecutionContext: {  
  2.     arguments: {  
  3.         0: ‘foo’, 1: ‘bar’,  
  4.         length: 2,  callee: function() //Points to foo function  
  5.     },  
  6.     a: ‘foo’, b: ‘bar’, c: undefined,  
  7.     z: function(), //Created z() function,  
  8.     d: undefined,  
  9.     this: window  
  10. }  


当ExecutionContext创建完成之后,函数开始从代码第一行执行,直到遇到遇到return或者函数结束。代码每次尝试使用变量,都会从ExecutionContext对象中读取。 

二、关于ExecutionContext栈 
在Javascript中,每一个单一的指令都是在ExecutionContext对象中被执行。我们已经知道,任何函数中所有的代码都将有一个ExecutionContext与之关联,无论该函数怎么被创建,怎么被执行。因此,任何函数中的每一个单一的语句都是在该函数的ExecutionContext中被执行。那些不属于任何函数的全局代码(执行的内联代码、通过<script>标签装载的代码、通过eval()函数执行的代码)被关联到一个叫做GlobalExecutionContext的上下文中。GlobalExecutionContext 
的工作机制非常类似ExecutionContext,但是没有方法参数,只包含2、3、4(this指向全局对象,常常是window对象)三步。通过以上得出结论:每一个Javascript语句运行都是在ExecutionContext中进行。程序运行中,有时候需要从一个函数跳转到另外一个函数(直接调用、DOM事件、定时器等)。由于每一个函数都有自己的ExecutionContext,所以这些函数的相互调用将形成一个上下文的栈,例如下面的代码: 

Js代码   收藏代码
  1. <script>  
  2.     function a() {  
  3.         function b() {  
  4.             var c = {  
  5.                 d: function() {  
  6.                     alert(1);  
  7.                 }  
  8.             };  
  9.             c.d();  
  10.         }  
  11.         b.call({});  
  12.     }  
  13.     a();  
  14. </script>  


当Javascript引擎即将执行alert()方法的时候,形成的ExecutionContext栈如下: 

Js代码   收藏代码
  1. d() Execution context.  
  2. b() Execution context.  
  3. a() Execution context.  
  4. Global Execution context.  


ExecutionContext栈中最重的部分发生在函数被定义的时候,要充分认识JavaScript的上下文的关键点是每一个声明的函数都是在ExecutionContext中被执行(前面的例子中:b()函数被创建和执行都是在a()函数的ExeuctionContext中)。每一次一个函数被创建,当前的 ExecutionContext栈就被保存到该函数自己的[[scope]]属性中,这个过程全部发生在函数创建的过程中,这个栈被保存和绑定到新创建的函数中,尽管以前的函数已经执行完(例如原来的函数将创建的函数作为返回值返回)。 

现在回头看一下第2步中函数域的创建: 
当函数被访问时,一个新的ExecutionContext栈被创建,然后将函数的ExecutionContext压入到前面提到的[[scope]]属性顶。这个栈我们也称之为 
“scope chain”.注意ExecutionContext栈可以并且常常是和calling stack不同的,后者是函数调用时被定义,前者是函数定义的时候就被定义。例如,函数a()调用函数b(),函数b()调用函数c(),那么这种“calling stack”可能在任何调试工具中被检查到,进一步,函数c()可能在函数d()中被创建,那么函数d()关联的ExecutionContext是“scope chain”的一部分,但是不属于“calling stack”。当函数内的代码查找一个变量,“scope chain”将被检查,引擎将在“scope chain”的第一个ExecutionContext中搜索该变量,这个ExecutionContext也是函数自己的ExecutionContext,通过搜索函数参数,变量等进行匹配,如果没有找到,引擎将继续在“scope chain”的下一个ExecutionContext中查找,一次类推直到“scope chain”的最后一个ExecutionContext,如果始终没有找到,就返回undefined作为该变量的值,如果在中间某个ExecutionContext中找到,就直接返回其中的值赋给变量。 

三、总结函数和其执行上下文的要点 
1.    this与函数不耦合也不是一个特殊的属性,更像一个普通的参数。它在函数调用的时候被定义,所以说相同的函数执行"this"是可以不一样的。 
2.    arguments不是一个数组,而是一个以数字作为属性名称的普通对象,所以它没有继承像push()、concat()和slice()的数组方法。 
3.    变量实际上在第3.3步被定义,无论变量定义到函数的什么地方,都要等到执行流到达该变量初始化指令代码的时候才初始化它们。这就是为什么我们的例子中d一开始指向undefined,当代码执行到函数第2行的时候d才指向3。函数及变量定义的提升 
4.    你能在一个函数被定义之前调用它,这依赖于3.2中ExecutionContext的创建(函数表达式不成立) 
5.    所有的内部声明函数都将在ExecutionContext阶段被创建,所以一个遥不可及的函数声明有可能始终被创建,比如: 

Js代码   收藏代码
  1. function foo() {  
  2.     if (false) {  
  3.         function bar() {alert(1);};  
  4.     }  
  5.     bar();  
  6. }  


以上代码在浏览器(IE8、Chrome和Safari5,Firefox不可以)中将正常运行,因为函数bar()在ExecutionContext阶段被创建,此时函数代码还没有开始执行,if条也没有被评估。 
6.    变量可以被隐藏。因为所有的步骤发生都是有顺序的,后发生的步骤有可能覆盖之前发生的步骤,比如:如果我们在函数签名中定义一个叫foo的参数,然后在函数体中声明一个函数也叫foo,那么当ExecutionContext创建完之后后面的foo变量将覆盖前面的foo变量。 
7.    闭包:一个函数能访问其“父函数”的变量。当访问一个变量在当前的 ExecutionContext中没有找到,在其“父函数”的ExecutionContext中找到,就形成了闭包。你甚至可以建立很多复杂的闭包通过使用当前函数的"父函数"、"祖父函数"等等中的数据。返回函数和闭包 
8.    Javascript中的全局变量。一个变量可以一直被查到“scope chain”的最后一项,即GlobalExecutionContext(这就是为什么全局变量访问相对较慢的原因,因为引擎将搜索完所有的关联的Context直到最后才访问GlobalExecutionContext)。同样你能使用类似全局变量:如果不用的函数拥有一个相同的ExecutionContext,那么声明在该ExecutionContext中的所有的变量都可以像全局变量一样被不同的函数访问 

四、总结 
Javascript代码实际上就是ExecutionContext和scope chains,该语言的大多数功能都可以从上下文的行为得到中提升。如果你习惯于在一个交互的上下文中设计你的项目,你的代码将更加的简单而且自然。例如,如果你心中有上下文的概念,一个mixin-based继承是很容易实现的;大部分加载的Javascript库都只依赖自身的管理模块上下文而不会污染全局环境。归纳起来,我们通过ExecutionContext和scope chains(而不是函数或者对象)来思考Javascript会使这门语言释放更大的能量,确保尽量深层次的去理解他们。 

猜你喜欢

转载自roomfourteen224.iteye.com/blog/2248632