Javascript中的函数和执行环境
函数是Javascript的主要组建部分,函数定义了诸如闭包、“this”关键字、全局变量、局部变量等诸多的特性。理解函数是真正理解Javascript工作机制的第一步。
一、ExecutionContext的创建
总所周知,函数能够访问声明在当前函数作用域“之外”的变量、全局变量、声明在函数内部的变量以及通过参数传进来的变量和指向“容器对象”的"this"变量。以上所有这些变量为我们的函数形成了一个“环境”,该“环境”定义了哪些变量和它们的值是可以被当前函数访问的。一部分“环境”是随着函数的定义而定义的,其他一些是函数访问的时候才定义的。
当一个函数被访问时,一个ExecutionContext被创建,ExecutionContext定义了函数“环境”大部分,接下来看一下ExecutionContext是怎么构建的(注意顺序):
利用伪代码演示例子:
function foo (a, b, c) { function z(){alert(‘Z!’);} var d = 3; } foo(‘foo’,’bar’);
1.arguments属性被创建,arguments属性是一个类似数组的对象,该对象的整数类型的属性分别引用传递给函数的参数值,顺序和传参时顺序一致。arguments对象包含length(参数个数)和callee(引用被调用的函数本身)属性。
ExecutionContext: { arguments: { 0: ‘foo’, 1: ‘bar’, length: 2, callee: function() //Points to foo function } }
2.函数域被创建,[[scope]]属性和我们上面所说的ExecutionContext,更多的细节后面会讲到。
3.变量实例化,分为3个子步骤(也是有顺序的):
3.1ExecutionContext会为每一个定义在函数签名中的参数定义一个属性,如果在前面已经创建的arguments对象中对应的位置有一个值,这个值被分配给该属性,否则,该属性值为undefined。
ExecutionContext: { arguments: { 0: ‘foo’, 1: ‘bar’, length: 2, callee: function() //Points to foo function }, a: ‘foo’, b: ‘bar’, c: undefined }
3.2扫描函数体检测其中声明的函数————FunctionDeclarations,这些声明的函数被创建并且作为属性分配给ExecutionContext,属性名就是该函数名称。
ExecutionContext: { arguments: { 0: ‘foo’, 1: ‘bar’, length: 2, callee: function() //Points to foo function }, a: ‘foo’, b: ‘bar’, c: undefined, z: function() //Created z() function }
3.3扫描函数体检测其中声明的变量,这些变量作为ExecutionContext的属性保存在ExecutionContext中并且被初始化成undefined。
ExecutionContext: { arguments: { 0: ‘foo’, 1: ‘bar’, length: 2, callee: function() //Points to foo function }, a: ‘foo’, b: ‘bar’, c: undefined, z: function(), //Created z() function, d: undefined }
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.作为构造函数(newmyFunction(1,2,3))。“this”是一个以myFunction.prototype作为原型的空对象。
ExecutionContext: { arguments: { 0: ‘foo’, 1: ‘bar’, length: 2, callee: function() //Points to foo function }, a: ‘foo’, b: ‘bar’, c: undefined, z: function(), //Created z() function, d: undefined, this: window }
当ExecutionContext创建完成之后,函数开始从代码第一行执行,直到遇到遇到return或者函数结束。代码每次尝试使用变量,都会从ExecutionContext对象中读取。
二、关于ExecutionContext栈
在Javascript中,每一个单一的指令都是在ExecutionContext对象中被执行。我们已经知道,任何函数中所有的代码都将有一个ExecutionContext与之关联,无论该函数怎么被创建,怎么被执行。因此,任何函数中的每一个单一的语句都是在该函数的ExecutionContext中被执行。那些不属于任何函数的全局代码(执行的内联代码、通过<script>标签装载的代码、通过eval()函数执行的代码)被关联到一个叫做GlobalExecutionContext的上下文中。GlobalExecutionContext
的工作机制非常类似ExecutionContext,但是没有方法参数,只包含2、3、4(this指向全局对象,常常是window对象)三步。通过以上得出结论:每一个Javascript语句运行都是在ExecutionContext中进行。程序运行中,有时候需要从一个函数跳转到另外一个函数(直接调用、DOM事件、定时器等)。由于每一个函数都有自己的ExecutionContext,所以这些函数的相互调用将形成一个上下文的栈,例如下面的代码:
<script> function a() { function b() { var c = { d: function() { alert(1); } }; c.d(); } b.call({}); } a(); </script>
当Javascript引擎即将执行alert()方法的时候,形成的ExecutionContext栈如下:
d() Execution context. b() Execution context. a() Execution context. Global Execution context.
ExecutionContext栈中最重的部分发生在函数被定义的时候,要充分认识JavaScript的上下文的关键点是每一个声明的函数都是在ExecutionContext中被执行(前面的例子中:b()函数被创建和执行都是在a()函数的ExeuctionContext中)。每一次一个函数被创建,当前的ExecutionContext栈就被保存到该函数自己的[[scope]]属性中,这个过程全部发生在函数创建的过程中,这个栈被保存和绑定到新创建的函数中,尽管以前的函数已经执行完(例如原来的函数将创建的函数作为返回值返回)。
现在回头看一下第2步中函数域的创建:
当函数被访问时,一个新的ExecutionContext栈被创建,然后将函数的ExecutionContext压入到前面提到的[[scope]]属性顶。这个栈我们也称之为
“scopechain”.注意ExecutionContext栈可以并且常常是和callingstack不同的,后者是函数调用时被定义,前者是函数定义的时候就被定义。例如,函数a()调用函数b(),函数b()调用函数c(),那么这种“callingstack”可能在任何调试工具中被检查到,进一步,函数c()可能在函数d()中被创建,那么函数d()关联的ExecutionContext是“scopechain”的一部分,但是不属于“callingstack”。当函数内的代码查找一个变量,“scopechain”将被检查,引擎将在“scopechain”的第一个ExecutionContext中搜索该变量,这个ExecutionContext也是函数自己的ExecutionContext,通过搜索函数参数,变量等进行匹配,如果没有找到,引擎将继续在“scopechain”的下一个ExecutionContext中查找,一次类推直到“scopechain”的最后一个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阶段被创建,所以一个遥不可及的函数声明有可能始终被创建,比如:
function foo() { if (false) { function bar() {alert(1);}; } bar(); }
以上代码在浏览器(IE8、Chrome和Safari5,Firefox不可以)中将正常运行,因为函数bar()在ExecutionContext阶段被创建,此时函数代码还没有开始执行,if条也没有被评估。
6.变量可以被隐藏。因为所有的步骤发生都是有顺序的,后发生的步骤有可能覆盖之前发生的步骤,比如:如果我们在函数签名中定义一个叫foo的参数,然后在函数体中声明一个函数也叫foo,那么当ExecutionContext创建完之后后面的foo变量将覆盖前面的foo变量。
7.闭包:一个函数能访问其“父函数”的变量。当访问一个变量在当前的ExecutionContext中没有找到,在其“父函数”的ExecutionContext中找到,就形成了闭包。你甚至可以建立很多复杂的闭包通过使用当前函数的"父函数"、"祖父函数"等等中的数据。返回函数和闭包
8.Javascript中的全局变量。一个变量可以一直被查到“scopechain”的最后一项,即GlobalExecutionContext(这就是为什么全局变量访问相对较慢的原因,因为引擎将搜索完所有的关联的Context直到最后才访问GlobalExecutionContext)。同样你能使用类似全局变量:如果不用的函数拥有一个相同的ExecutionContext,那么声明在该ExecutionContext中的所有的变量都可以像全局变量一样被不同的函数访问
四、总结
Javascript代码实际上就是ExecutionContext和scopechains,该语言的大多数功能都可以从上下文的行为得到中提升。如果你习惯于在一个交互的上下文中设计你的项目,你的代码将更加的简单而且自然。例如,如果你心中有上下文的概念,一个mixin-based继承是很容易实现的;大部分加载的Javascript库都只依赖自身的管理模块上下文而不会污染全局环境。归纳起来,我们通过ExecutionContext和scopechains(而不是函数或者对象)来思考Javascript会使这门语言释放更大的能量,确保尽量深层次的去理解他们。