本文详细解析JavaScript引擎的执行流程,涵盖按HTML文档流顺序加载、预编译阶段的变量与函数声明处理、代码块隔离与全局作用域共享,以及利用window.onload改变执行时机。通过具体代码示例说明变量提升、函数声明与表达式声明的区别,并探讨document.write()输出的风险,帮助开发者掌握JS执行细节以避免常见错误。
JavaScript执行原理概述
直接切入核心,JavaScript的运行机制主要包含以下几个关键方面:
HTML文档流顺序执行
1、遵循HTML文档流顺序执行JavaScript代码
浏览器解析页面结构和信息时,是严格按照HTML文档流从上至下进行的。JavaScript代码作为嵌入脚本,属于HTML文档的一部分,因此其加载和执行顺序完全取决于<script>标签在文档中出现的位置。
若通过<script>标签的src属性引入外部.js文件,该文件也会严格遵循其内部语句出现的顺序执行。这一过程被视为文档加载的一部分,并不会因为文件是外部的而延迟执行。
预编译与执行顺序关系
2、预编译阶段与执行顺序的关系
首先观察以下代码片段:
<script type="text/javascript">
function hello() {
alert("hello");
}
hello();
function hello() {
alert("hello world");
}
hello();
</script>
上述JavaScript代码的实际输出结果是两次“hello world”,而非先输出“hello”再输出“hello world”。这并非JavaScript完全按顺序解释执行的结果,因为在正式解释执行前,JS引擎会进行预编译。在预编译阶段,引擎会优先处理函数定义,并创建所有var声明的变量,初始值设为undefined,以此提升执行效率。换言之,上述代码在预编译后实际上等效于以下结构:
<script type="text/javascript">
var hello = function() {
alert("hello");
};
hello = function() {
alert("hello world");
};
hello();
hello();
</script>
从上述代码可以看出,函数本质上也是一种变量,可以被重新赋值。为了避免前文提到的覆盖问题,可以将代码拆分为两个独立的.js文件:
<script type="text/javascript">
hello();
function hello() {
alert("hello");
}
// hello();
</script>
<script type="text/javascript">
function hello() {
alert("hello world");
}
hello();
</script>
在第一个文件中,即使将hello()函数调用放在function定义之前,依然能输出正确结果。
<script type="text/javascript">
hello();
var hello = function() {
alert("hello");
};
// hello();
</script>
然而,如果采用上述方法定义function函数,则会引发错误,具体报错信息如图1所示:
报错信息为“hello is not a function”。这是因为在预编译阶段,虽然var声明的变量被优先处理,但其值仅为undefined。当执行hello()时,由于此时hello变量的类型尚未确定(仍为undefined),因此报错。尽管代码中后续定义了该函数,但由于定义位置在调用之后,程序执行到调用处时尚未触及定义部分,导致调用失败。
再来看下面这段代码:
<script type="text/javascript">
hello();
function hello() {
alert("hello");
}
// hello();
</script>
尽管函数调用位于定义之前,但由于此处使用的是function关键字进行定义,与var不同,function在定义时即完成了值的赋值,因此代码可以正常运行。
总结:
当JavaScript引擎解析脚本时,会在预编译期对所有声明的变量和函数进行处理。具体处理逻辑如下:
(1) 在执行前会进行类似“预编译”的操作:首先创建一个当前执行环境下的活动对象,将用var声明的变量作为该对象的属性,但此时这些变量的赋值均为undefined;同时,将以function定义的函数也添加为活动对象的属性,其值即为函数的定义本身。
(2) 在解释执行阶段,遇到变量解析需求时,会首先从当前执行环境的活动对象中查找;若未找到且该执行环境的拥有者具有prototype属性,则从prototype链中查找;否则按作用域链查找。遇到var a = ...这类语句时,会对相应变量进行赋值(注意:变量的赋值是在解释执行阶段完成的,若在此之前使用变量,其值将为undefined)。
(3) 综上所述,可以概括为一句话:变量的声明发生在预编译期,而变量的初始化发生在运行期。
<script type="text/javascript">
alert(a); // 在预编译期间a变量已经加载,但是用var定义,所以赋值为undefined先,故这里输出undefined。
var a = 1; // 这里给前面的没有赋值的a进行赋值为1
alert(a); // 这里输出的a已经是前面赋值过的,所以输出1。
</script>
对于上述代码,输出结果为:先输出undefined,后输出1。具体分析见代码注释。
虽然变量和函数声明可以出现在文档的任意位置,但良好的编码习惯是在所有JavaScript代码之前声明全局变量和函数,并对变量进行初始化赋值。在函数内部,也应遵循先声明变量、后引用的原则。
代码块执行机制
3、按代码块执行JavaScript代码
所谓代码块,是指由<script>标签分隔的代码段。JavaScript解释器在执行脚本时,是按块为单位进行的。通俗地说,当浏览器解析HTML文档流遇到<script>标签时,JavaScript解释器会等待该代码块完全加载,先对代码块进行预编译,然后再执行。执行完毕后,浏览器继续解析后续的HTML文档流,同时JavaScript解释器准备处理下一个代码块。由于JavaScript是按块执行的,因此如果在当前代码块中调用后续代码块中声明的变量或函数,将会提示语法错误。
<script>
alert(a);
</script>
<script>
var a = 1;
</script>
上述代码包含两个代码块,执行顺序为先完成第一个代码块,再执行第二个代码块。在执行第一个代码块时,由于变量a尚未声明,因此报错,报错信息为:“a is not defined”。
<script>
var a = 1;
</script>
<script>
alert(a);
</script>
尽管JavaScript是按块执行的,但不同代码块均属于同一个全局作用域,这意味着块之间的变量和函数是可以共享的。因此,在运行上述两个代码块时,虽然分为两个块,但第一段代码运行后,变量a已存在于全局作用域中。当运行到第二个代码块时,输出的a变量可以成功调用全局作用域中的a,因此不会出现问题。
事件机制改变执行顺序
4、借助事件机制改变JavaScript执行顺序
由于JavaScript按块处理代码,同时遵循HTML文档流的解析顺序,因此在上述示例中会看到语法错误。但当文档流加载完毕后,再次访问则不会出现此类错误。为了安全起见,我们通常在页面初始化完毕之后才允许JavaScript代码执行,这样可以避免网络速度对JavaScript执行的影响,同时也规避了HTML文档流对JavaScript执行的限制。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>javascript</title>
<script>
window.onload = function() {
alert(a);
};
</script>
<script>
var a = 1;
alert("bb");
</script>
</head>
<body>
</body>
<script>
alert("cc");
</script>
</html>
window.onload = function()表示先在触发事件上绑定一个函数,并不立即执行,而是在整个页面加载完成后再开始执行该事件及对应的function。因此,在window.onload执行之前,部分变量已经加载到了全局区中,所以没有问题。上述代码的输出结果为:先输出“bb”,再输出“cc”,最后输出a。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>javascript</title>
<script>
window.onload = function() {
alert(a);
};
// 上面的onload不会执行,只会执行下面的onload
window.onload = function() {
alert("onload2");
};
</script>
<script>
var a = 1;
alert("bb");
</script>
</head>
<body>
</body>
<script>
alert("cc");
</script>
</html>
如果在一个页面中存在多个window.onload事件处理函数,则只有最后一个才是有效的(如上述代码所示)。为了解决这个问题,可以将所有脚本或调用函数放在同一个onload事件处理函数中,如下面的代码所示:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>javascript</title>
<script>
window.onload = function() {
// 放到一起
alert(a);
alert("onload2");
};
</script>
<script>
var a = 1;
alert("bb");
</script>
</head>
<body>
</body>
<script>
alert("cc");
</script>
</html>
脚本输出执行顺序
5、JavaScript输出脚本的执行顺序
在JavaScript开发中,经常使用document对象的write()方法输出JavaScript脚本。document.write()方法先将输出的脚本字符串写入到脚本所在的文档位置,浏览器在解析完document.write()所在文档内容后,继续解析document.write()输出的内容,然后才按顺序解析后续的HTML文档。也就是说,JavaScript脚本输出的代码字符串会在输出后马上被执行。请注意,使用document.write()方法输出的JavaScript脚本字符串必须放在同时被输出的<script>标签中,否则JavaScript解释器无法识别这些合法的JavaScript代码,而会将其作为普通字符串显示在页面文档中。但是,通过document.write()方法输出脚本并执行也存在一定的风险,因为不同JavaScript引擎对其执行顺序的处理不同,同时不同浏览器在解析时也可能出现Bug。
以上所述是小编给大家介绍的JavaScript语句的执行过程,希望对大家有所帮助。