JavaScript征途(有JS经验者可以一读,有犀牛书没有讲到的内容) 试读

来源:互联网 发布:数组属于什么数据结构 编辑:程序博客网 时间:2024/04/29 15:47

http://www1.huachu.com.cn/read/readbook.asp?bookid=10109449

 

 

第1章 与初学者谈谈JavaScript学习

1.5 JavaScript解析机制

        JavaScript解析过程可以分为编译和执行两个阶段。编译也就是我们常说的JavaScript预处理(即预编译)。在预编译期,JavaScript解释器将完成对JavaScript代码的预处理,也就是说把JavaScript脚本代码转换成字节码。在执行期,JavaScript解释器借助执行期环境把字节码生成机械码,并按顺序执行,完成程序设计的任务。

1.5.1 

预编译

        JavaScript是一种解释型语言,而不是编译型语言。所谓解释型语言,就是代码在执行时才被解释器一行行动态编译和执行,而不是在执行之前就完成编译。简单说,解释型语言就是边编译边执行,而编译型语言是先编译后执行,两者的操作过程不同。

    当程序被编译时,需要一个叫做编译器的程序来完成所有工作。一般编译器可以包括下面组件(如图1-3所示)。

l        符号表:在其中存储所有的符号及其信息,如类型、范围等。

l        词法分析器:其功能是将字符流(即脚本字符串)转换为记号(如关键词、操作符等)。

l        语法分析器:其功能是读取记号流,并建立语法树。

l        语义检查器:用来检查语法树的语义错误。

l        中间代码生成器:用来把语法树转换为中间代码。

l        代码优化器:用来优化中间代码。

l        代码生成器:用来把中间代码生成二进制字节码。

图1-3  编译器构成和工作流程示意图

根据上图,我们可以看到,程序的一般编译步骤分为:词法分析、语法分析、语义检查、代码优化和生成字节码。但是,对于JavaScript这类解释型语言来说,通过词法分析和语法分析,并建立语法树之后,就开始解释执行了,而不是完全生成字节码之后,再调用虚拟机来执行这些编译好的字节码。

在词法分析过程中,JavaScript解释器先把脚本代码的字符流转换为记号流,例如:

把字符流:

a = (b - c);

转换为记号流:

NAME "a"

EQUALS

OPEN_PARENTHESIS

NAME "b"

MINUS

NAME "c"

CLOSE_PARENTHESIS

SEMICOLON

词法分析器是编译器中与源程序直接接触的部分,因此词法分析器可以实现:

l        去掉注释,自动生成文档。

l        提供错误位置(可以通过记录行号来提供),当字符流变成词法记号流以后,就没有了行的概念。

l        完成预处理,如C语言中的宏定义等。

词法结构是JavaScript语言基础(详细讲解请参阅第3章内容),至于词法分析的实现就比较复杂,这里就不再深入研究,读者只需要简单了解它的工作机制即可。

词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,词法分析器不会读取所有的词法记号,然后再使用语法分析器来处理,通常情况下,每取一个词法记号,就送入语法分析器进行分析(如图1-4所示)。

图1-4  词法分析和语法分析示意图

词法分析是对JavaScript脚本代码进行逐一分析的过程,它相当于语言翻译,例如,把英文逐词逐句地译成中文,英文就是源代码,而中文就是代码的记号了。

语法分析的过程就是把词法分析所产生的记号生成语法树。通俗地说就是把从程序中收集的的信息存储到数据结构中。请注意,编译中的数据结构包括两种:符号表和语法树。

l        符号表:就是在程序中用来存储所有符号的一个表,包括所有的字符串变量、直接量字符串,还有函数和类。

l        语法树:就是程序结构的一个树形表示,并将使用这个树形结构来生成中间代码。

例如,下面是一个简单的条件结构和输出信息代码段,被语法分析器转换为语法树之后,如图1-5所示。

if(typeof a == "undefined" ){

   a = 0;

}

else{

   a = a;

}

alert(a);

图1-5  语法树结构示意图

当JavaScript解释器在构造语法树的时候,如果发现无法构造,就会报语法错误,并结束整个代码块的解析。对于传统强类型语言来说,通过语法分析,构造出语法树后,翻译出来的句子可能还会有模糊不清的地方,还需要进一步的语义检查。语义检查的主要部分是类型检查,例如,函数的实参和形参类型是否匹配。但是,对于弱类型语言来说,这一步就没有了。

1.5.2 

执行期

经过编译阶段的准备,JavaScript代码在内存中已经被构建为语法树,然后JavaScript引擎就会根据这个语法树结构边解释边执行了。

在解释过程中,JavaScript引擎是严格按着作用域机制来执行的。JavaScript语法采用的是词法作用域,也就是说JavaScript的变量和函数作用域是在定义时决定的,而不是执行时决定的,由于词法作用域取决于源代码结构,所以JavaScript解释器只需要通过静态分析就能确定每个变量、函数的作用域,这种作用域也称为静态作用域。

当JavaScript解释器执行每个函数时,先创建一个执行环境,在这个虚拟环境中创建一个调用对象,在这个对象内存储着当前域中所有局部变量、参数、嵌套函数、外部引用和父级引用列表upvalue等语法分析结构。

实际上,通过声明语句定义的变量和函数在预编译期的语法分析中就已经存储到符号表中了,然后把它们与调用对象中的同名属性进行映射即可。调用对象的生命周期与函数的生命周期是一致的,当函数调用完毕且没有外部引用的情况下,会自动被JavaScript引擎当做垃圾进行回收。

另外,JavaScript解释器通过作用域链把多个嵌套的作用域串连在一起,并借助这个链条帮助JavaScript解释器检索变量的值。这个作用域链相当于一个索引表,并通过编号来存储它们的嵌套关系。当JavaScript解释器检索变量的值,会按着这个索引编号进行快速查找,直到找到全局对象为止,如果没有找到值,则传递一个特殊的undefined值。

如果函数引用了外部变量的值,则JavaScript解释器会为该函数创建一个闭包体,闭包体是一个完全封闭和独立的作用域,它不会在函数调用完毕后就被JavaScript引擎当做垃圾进行回收。闭包体可以长期存在,因此开发人员常把闭包体当做内存中的蓄水池,专门用来长期保存变量的值。

只有当闭包体的外部引用被全部设置为null值时,该闭包才会被回收。当然,也容易引发垃圾泛滥,甚至出现内存外溢的现象。

 

 

1.6 JavaScript执行顺序

    上一节是从JavaScript引擎的解析机制来探索JavaScript的工作原理,下面我们以更形象的示例来说明JavaScript代码在页面中的执行顺序。如果说,JavaScript引擎的工作机制比较深奥是因为它属于底层行为,那么JavaScript代码执行顺序就比较形象了,因为我们可以直观感觉到这种执行顺序,当然JavaScript代码的执行顺序是比较复杂的,所以在深入JavaScript语言之前也有必要对其进行剖析。

1.6.1  按HTML文档流顺序执行JavaScript代码

    首先,读者应该清楚,HTML文档在浏览器中的解析过程是这样的:浏览器是按着文档流从上到下逐步解析页面结构和信息的。JavaScript代码作为嵌入的脚本应该也算做HTML文档的组成部分,所以JavaScript代码在装载时的执行顺序也是根据脚本标签<script>的出现顺序来确定的。例如,浏览下面文档页面,你会看到代码是从上到下逐步被解析的。

<script>

alert("顶部脚本");

</script>

<html><head>

<script>

alert("头部脚本");

</script>

<title></title>

</head>

<body>

<script>

alert("页面脚本");

</script>

</body></html>

<script>

alert("底部脚本");

</script>

如果通过脚本标签<script>的src属性导入外部JavaScript文件脚本,那么它也将按照其语句出现的顺序来执行,而且执行过程是文档装载的一部分。不会因为是外部JavaScript文件而延期执行。例如,把上面文档中的头部和主体区域的脚本移到外部JavaScript文件中,然后通过src属性导入。继续预览页面文档,你会看到相同的执行顺序。

<script>

alert("顶部脚本");

</script>

<html>

<head>

<script src="head.js"></script>

<title></title>

</head>

<body>

<script src="body.js"></script>

</body>

</html>

<script>

alert("底部脚本");

</script>

1.6.2  预编译与执行顺序的关系

当JavaScript引擎解析脚本时,它会在预编译期对所有声明的变量和函数进行处理。所以,就会出现当JavaScript解释器执行下面脚本时不会报错:

alert(a);                      // 返回值undefined

var a =1;

alert(a);                       // 返回值1

由于变量声明是在预编译期被处理的,所以在执行期间对于所有代码来说,都是可见的。但是,你也会看到,执行上面代码,提示的值是undefined,而不是1。这是因为,变量初始化过程发生在执行期,而不是预编译期。在执行期,JavaScript解释器是按着代码先后顺序进行解析的,如果在前面代码行中没有为变量赋值,则JavaScript解释器会使用默认值undefined。由于在第二行中为变量a赋值了,所以在第三行代码中会提示变量a的值为1,而不是undefined。

同理,下面示例在函数声明前调用函数也是合法的,并能够被正确解析,所以返回值为1。

f();                           // 调用函数,返回值1

function f(){

    alert(1);

}

但是,如果按下面方式定义函数,则JavaScript解释器会提示语法错误。

f();                           // 调用函数,返回语法错误

var f = function(){

    alert(1);

}

这是因为,上面示例中定义的函数仅作为值赋值给变量f,所以在预编译期,JavaScript解释器只能够为声明变量f进行处理,而对于变量f的值,只能等到执行期时按顺序进行赋值,自然就会出现语法错误,提示找不到对象f。

虽然变量和函数声明可以在文档任意位置,但是良好的习惯应该是在所有JavaScript代码之前声明全局变量和函数,并对变量进行初始化赋值。在函数内部也是先声明变量,然后再引用。

1.6.3  按块执行JavaScript代码

所谓代码块就是使用<script>标签分隔的代码段。例如,下面两个<script>标签分别代表两个JavaScript代码块。

<script>

// JavaScript代码块1

var a =1;

</script>

<script>

// JavaScript代码块2

function f(){

    alert(1);

}

</script>

JavaScript解释器在执行脚本时,是按块来执行的。通俗地说,就是浏览器在解析HTML文档流时,如果遇到一个<script>标签,则JavaScript解释器会等到这个代码块都加载完后,先对代码块进行预编译,然后再执行。执行完毕后,浏览器会继续解析下面的HTML文档流,同时JavaScript解释器也准备好处理下一个代码块。

由于JavaScript是按块执行的,所以如果在一个JavaScript块中调用后面块中声明的变量或函数就会提示语法错误。例如,当JavaScript解释器执行下面代码时就会提示语法错误,显示变量a未定义,对象f找不到。

<script>

// JavaScript代码块1

alert(a);

f();

</script>

<script>

// JavaScript代码块2

var a =1;

function f(){

    alert(1);

}

</script>

虽然说,JavaScript是按块执行的,但是不同块都属于同一个全局作用域,也就是说,块之间的变量和函数是可以共享的。


1.6.4  借助事件机制改变JavaScript执行顺序

由于JavaScript是按块处理代码,同时又遵循HTML文档流的解析顺序,所以在上面示例中会看到这样的语法错误。但是当文档流加载完毕,如果再次访问就不会出现这样的错误。例如,把访问第2块代码中的变量和函数的代码放在页面初始化事件函数中,就不会出现语法错误了。

<script>

// JavaScript代码块1

window.onload = function(){    // 页面初始化事件处理函数

    alert(a);

    f();

}

</script>

<script>

// JavaScript代码块2

var a =1;

function f(){

    alert(1);

}

</script>

为了安全起见,我们一般在页面初始化完毕之后才允许JavaScript代码执行,这样可以避免网速对JavaScript执行的影响,同时也避开了HTML文档流对于JavaScript执行的限制。

注意

如果在一个页面中存在多个windows.onload事件处理函数,则只有最后一个才是有效的,为了解决这个问题,可以把所有脚本或调用函数都放在同一个onload事件处理函数中,例如:

window.onload = function(){

    f1();

    f2();

    f3();

}

而且通过这种方式可以改变函数的执行顺序,方法是:简单地调整onload事件处理函数中调用函数的排列顺序。

除了页面初始化事件外,我们还可以通过各种交互事件来改变JavaScript代码的执行顺序,如鼠标事件、键盘事件及时钟触发器等方法,详细讲解请参阅第14章的内容。

1.6.5  JavaScript输出脚本的执行顺序

在JavaScript开发中,经常会使用document对象的write()方法输出JavaScript脚本。那么这些动态输出的脚本是如何执行的呢?例如:

document.write('<script type="text/javascript">');

document.write('f();   ');

document.write('function f(){  ');

document.write('    alert(1);   ');

document.write('}  ');

document.write('<\/script> ');

运行上面代码,我们会发现:document.write()方法先把输出的脚本字符串写入到脚本所在的文档位置,浏览器在解析完document.write()所在文档内容后,继续解析document.write()输出的内容,然后才按顺序解析后面的HTML文档。也就是说,JavaScript脚本输出的代码字符串会在输出后马上被执行。

请注意,使用document.write()方法输出的JavaScript脚本字符串必须放在同时被输出的<script>标签中,否则JavaScript解释器因为不能够识别这些合法的JavaScript代码,而作为普通的字符串显示在页面文档中。例如,下面的代码就会把JavaScript代码显示出来,而不是执行它。

document.write('f();   ');

document.write('function f(){  ');

document.write('    alert(1);   ');

document.write('); ');

但是,通过document.write()方法输出脚本并执行也存在一定的风险,因为不同JavaScript引擎对其执行顺序不同,同时不同浏览器在解析时也会出现Bug。

l        问题一,找不到通过document.write()方法导入的外部JavaScript文件中声明的变量或函数。例如,看下面示例代码。

document.write('<script type="text/javascript" src="test.js">

<\/script>');

document.write('<script type="text/javascript"> ');

document.write('alert(n);  '); // IE提示找不到变量n

document.write('<\/script> ');

alert(n+1);                     // 所有浏览器都会提示找不到变量n

外部JavaScript文件(test.js)的代码如下:

var n = 1;

分别在不同浏览器中进行测试,会发现提示语法错误,找不到变量n。也就是说,如果在JavaScript代码块中访问本代码块中使用document.write()方法输出的脚本中导入的外部JavaScript文件所包含的变量,会显示语法错误。同时,如果在IE浏览器中,不仅在脚本中,而且在输出的脚本中也会提示找不到输出的导入外部JavaScript文件的变量(表述有点长和绕,不懂的读者可以尝试运行上面代码即可明白)。

l        问题二,不同JavaScript引擎对输出的外部导入脚本的执行顺序略有不同。例如,看下面示例代码。

<script type="text/javascript">

document.write('<script type="text/javascript" src="test1.js">

<\/script>');

document.write('<script type="text/javascript"> ');

document.write('alert(2);')

document.write('alert(n+2);');

document.write('<\/script>');

</script>

<script type="text/javascript">

alert(n+3);

</script>

外部JavaScript文件(test1.js)的代码如下所示。

var n = 1;

alert(n);

在IE浏览器中的执行顺序如图1-6所示。

图1-6  IE 7浏览器的执行顺序和提示的语法错误

在符合DOM标准的浏览器中的执行顺序与IE浏览器不同,且没有语法错误,如图1-7所示的是在Firefox 3.0浏览器中的执行顺序。

图1-7  Firefox 3浏览器的执行顺序和提示的语法错误

解决不同浏览器存在的不同执行顺序,以及可能存在Bug。我们可以把凡是使用输出脚本导入的外部文件,都放在独立的代码块中,这样根据上面介绍的JavaScript代码块执行顺序,就可以避免这个问题。例如,针对上面示例,可以这样设计:

<script type="text/javascript">

document.write('<script type="text/javascript" src="test1.js"><\/script>');

</script>

<script type="text/javascript">

document.write('<script type="text/javascript"> ');

document.write('alert(2);  ') ;        //提示2

document.write('alert(n+2); ');         //提示3

document.write('<\/script> ');

alert(n+3);                            //提示4

</script>

<script type="text/javascript">

alert(n+4);                             //提示5

</script>

这样在不同浏览器中都能够按顺序执行上面代码,且输出顺序都是1、2、3、4和5。存在问题的原因是:输出导入的脚本与当前JavaScript代码块之间的矛盾。如果单独输出就不会发生冲突了。

原创粉丝点击