JavaScript文件的执行顺序

562 阅读4分钟

这是来自真实的鹅厂秋招面试题。更多请看大厂前端面经

JavaScript是一种描述型脚本语言。它不同于Java或C#等编译性语言,不需要进行编译成中间语言,而是由浏览器直接进行动态解析与执行。也正是由于这个特性,JavaScript代码在HTML文件中的执行顺序和其他语言就不太一样。

1.代码块

代码块是指由<script>标签分割的JavaScript代码组成的代码段。浏览器是按照代码块来对JavaScript代码进行编译和执行的,代码块之间相互独立,但是变量方法共享。

那么,一个<script>标签形成一个块说明前面的JavaScript代码运行错误并不影响后面<script>标签里的代码运行。

什么意思呢,看个例子就明白了👇:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    console.log(s); //s未定义,控制台报错,后面的都不执行了
    console.log('test1'); 
    var test1 = '我是test1';
  </script>
  <script>
    console.log('test2'); //控制台输出test2
    console.log(test1); //输出undefined。并不是报错未定义
  </script>
</head>

<body>
  
</body>

</html>

从以上的例子可以看出:

  • 代码块一报错并不影响代码块二运行,这就是代码块间的独立性。

  • 代码块二能够调用到代码块一的变量,有人把这个叫做块间共享性。

那为什么代码块二拿到test1并且test1undefined而不是test1原来的值'我是test1'呢?

  1. 为什么代码块二拿到test1

    因为JavaScript语言只有全局作用域(在浏览器中运行是window)函数作用域,所以在全局作用域里定义的变量都能够访问。而同一个HTML文件里的JavaScript代码都是浏览器的同一个全局作用域window,所以代码块之间可以相互访问。

  2. 为什么是undefined:

  • 浏览器解析阶段:代码执行之前使用var定义的变量会声明提前(声明式函数也会声明提前),先定义后执行时赋值,所以在运行到console.log(s)之前变量test1已经被声明为undefined

  • 浏览器执行阶段:执行到console.log(s)因为s未定义报错,后面的代码都不执行了。此时的test1就没有完成将'我是test1'赋值给test1变量的操作,所以代码块二拿到undefined

ES6的letconst都具有块级作用域,所以将上面代码块一的var改成let后,代码块二就访问不到test1会在控制台报错表示变量未定义。

2.声明提前

JavaScript的函数定义分为两种:声明式函数和赋值式函数。

//声明式
function fn() {
    
}
//赋值式函数
var fn = function (){
    
}

在浏览器解析阶段声明式函数会和之前的变量一样先被声明,未赋值。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    fn(); //'1'
    function fn() {
      console.log('1');
    }
    function fn() {
      console.log('2');
    }
  </script>
  <script>
    fn(); //声明
    var fn = function () {
      console.log('赋值');
    }
    function fn() {
      console.log('声明');
    }
    fn(); //赋值
  </script>
</head>

<body>

</body>

</html>

因为function这种形式定义函数会在浏览器预解析的时候声明提前,所以fn函数调用在函数定义之前也可以。

ok,来看最后一个例子:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    fn(); //报错,未定义的fn函数
  </script>
  <script>
    function fn() {
      console.log('fn');
    }
  </script>
</head>

<body>

</body>

</html>

声明函数不是在预解析就被提前了,那为什么会报错呢?

上面说了JS引擎是按照代码块顺序执行的,其实完整的说应该是按照代码块来进行预处理和执行的。也就是说预处理的只是执行到的代码块的声明函数和变量,而对于还未加载的代码块,是没法进行预处理的,这也是边编译边处理的核心所在。