该系列文章的内容主要来自: Pro JavaScript with Mootools.作者: Mark Joseph Obceca
本节,我们将揭开函数内部实现的帷幕一角,看一眼 js解释器遇到函数定义,函数调用时,它到底做了什么工作。我们不会深入到解释器的实现技术细节,我们主要关注那些帮助我们理解函数的定义和调用的内容。
注意:各家解释器的实现各不相同,但是ECMAScript规范描述了解释器实现函数的一般规则。我们根据ECMAScript的官方指导去深入函数内部,小探一把。
可执行代码 和 执行环境 (contexts:有翻译成环境的,有翻译成上下文的,我就把它翻成环境吧,可少敲一个字)
Javascript 把可执行代码分成了三类:
说起来比较抽象,还是上例子吧:
//this is global code var name='John'; var age =20; function add(a,b){ //this is function code var result=a+b; return result; } (function(){ //this is function code var day='Tuesday'; var time = function(){ //this is also function code , //but it is separate from the code above return day; }; })(); //this is eval code eval("alert('yay!');");
变量 name,age,和我们生成的大部分函数,都出现在顶级,也就是说 它们是Global代码。然而函数内部的代码,我们称之为Function代码。
当一个函数定义在其他函数的内部时,该函数的代码也会被看作一段独立的Function代码。
为什么Javascript把可执行代码分成不同的类型呢? 答案就是:解释器为了在解释代码时,能够保持跟踪当前解释执行到了代码的什么位置。具体的说就是:js解释器使用一种叫做: 执行上下文的内部机制来对代码的解释执行进行跟踪。
在执行一段脚本的过程中,js会生成和进入多个不同的执行环境,执行环境不仅跟踪代码的位置,同时也会保存当前的数据变量以保证代码的正确执行。
一个JS程序,至少有一个执行环境,一般称之为: global执行环境。当js解释器开始执行你编写的程序,解释器就进入了global执行环境,并且开始使用当前执行环境解释执行代码。当js解释器遇到函数调用,解释器就生成一个新的执行环境,然后进入该执行环境,并且使用这个环境解释执行函数代码。当函数执行结束或返回一个值,js解释器退出这个执行环境,返回上一个环境。
说起来有点乱,还是用示例代码会清楚些:
var a =1; var add = function(a,b){ return a+b; }; var callAdd = function(a,b){ return add(a,b); }; add(a,2); callAdd(1,2);
让我们从js解释器的视角一步步的去执行上面的代码:
JS解释器中的几个内置的对象和执行环境联系紧密,直接影响脚本程序的执行。
变量和变量初始化
和执行环境联系紧密的第一个内置对象就是 variable object。 每个执行环境都有自己的variable object。该对象用来跟踪该执行环境下定义的全部变量。
js中生成变量的过程叫做变量初始化。应为js是一种文法作用域的语言。因此,变量的作用域根据变量在代码中初始化的位置确定。这个规则唯一的例外就是:由省略了var关键字定义的全局变量。
var friut = 'banana'; var add = function(a,b){ var localResult = a+b; globalResult = localResult; return localResult; }; add(1,2);
在上面的代码片段中,变量 friut和 add是全局作用域的。可以在整个脚本中使用。localResult和a,b都是局部作用域的。仅能在add函数内使用。而globalResult尽管定义在add函数中,但是省略了var 关键字,就成了全局作用域的变量了。
当js解释器进入一个执行环境时,它做的第一件事就是变量初始化。 解释器先生成一个variable object, 然后检查当前环境下的var 声明。接着这些变量被生成,添加到variable object的属性中,赋值为: undefined。对我们上面的例子代码而言,我们可以说: 变量friut 和add由global环境的variable object初始化。而变量a,b, localResult则由add函数的本地执行环境的variable object初始化。而变量 globalResult比较诡异,我们稍候讨论它的实现。
关于变量初始化,要牢记一点:它和执行环境休戚相关。若你还有印象的话,javascript把可执行代码分成了三类: global代码,function代码和eval代码。我们可以说: js 提供了三种执行环境: global 执行环境,function执行 环境和eval执行环境。
由于变量的初始化和执行环境的variable object有关,因此js中,我们有三种类型的变量: global 变量,function本地变量,和eval代码的变量。这引出了js中另外一个让很多人困惑的地方:js没有块左右域。在其他的类似于C的语言里,包含在一对括号里的代码,被称做 块,块有着自己独立的左右域。 而js中的是没有块左右域的,解释器进入新的执行环境,在当前执行环境定义的任何变量都会被初始化。不管是否在块中。
举例如下:
var x=1; if(false){ var y=2; } console.log(x);//1 console.log(y);//undefined
在一个有块作用域的语言中,console.log(y)一行的执行会报错,因为你试图访问一个没初始化的变量(因为 var y=2永远不会被执行)。但是js却并没报错,而是告诉我们y的值是undefined, undefined也就是一个变量被初始化了,但是没给值。js解释器的这种行为有点独特,是吧?
作用域链和闭包
可执行代码和执行环境是一一绑定的,从js解释器的角度看:
解释器每进入一种代码,就会生成一个当前代码的执行环境。每个执行环境都有自己的variable object属性去跟踪当前环境中定义的全部变量,在global执行环境中,variable object 又被称作 window 或 global对象, 当生成执行环境后,解释器,对当前环境定义的变量进行解析,把当前执行环境定义的变量放入variable object中,作为它的属性。当从一个执行环境,进入另外一个执行环境时,就发生了执行环境的嵌套,而执行环境用另外一个内置的属性scope chain去保存这个嵌套。
Scope chain: global variable object<--outer variable object<--local variable object
执行环境对当前环境中变量的辨识就是通过scope chain进行。对于省略var 关键子定义的变量,由于不记录在local variabe object中,就顺着scope chain回溯到global中,若是还没找到,就在global中生成一个新的属性。
每个函数在定义时,该函数会生成一个内置的scope属性,该属性是定义该函数时,执行环境的variable object的嵌套。当调用这个函数时,解释器,生成该函数的执行环境,并用根据该函数的scope属性生成了这个执行环境的作用域链。而闭包的定义和调用一般是两个执行环境。这就是闭包产生的低层机理。
对于用new Function()产生的函数定义,该函数的scope属性里只有一个东西就是global variable object.
还是上示例回味吧:
var fruit = 'banana'; var animal = 'cat'; function sayFruit(){ var fruit = 'apple'; console.log(fruit); // 'apple' console.log(animal); // 'cat' }; console.log(fruit); // 'banana' console.log(animal); // 'cat' sayFruit();
var fruit = 'banana'; function outer(){ var fruit = 'orange'; function inner(){ console.log(fruit); // 'orange' }; inner(); }; outer();
var fruit = 'banana'; function outer(){ var fruit = 'orange'; var inner = new Function('console.log(fruit);'); inner(); // 'banana' }; outer();
var fruit = 'banana'; var inner; (function(){ var fruit = 'apple'; inner = function(){ console.log(fruit); }; })(); console.log(fruit); // 'banana' inner(); // 'apple'