🗒️预处理

两个 JavaScript 语法的全局机制:预处理和指令序言。

这个机制对于解释一些 JavaScript 的语法现象非常重要。不理解预处理机制,就无法理解 var 等声明类语句的行为。预处理会把作用域提前,有些情况也会赋值。

宏任务中可能执行的代码包括函数、脚本和模块。函数体其实也是一个语句的列表,和脚本和模块比起来,它里面的语句列表中多了 return 语句可以用。函数体、脚本和模块可以使用的语句,如下:

在 JavaScript 执行前,会对脚本、模块和函数体中的语句进行预处理,它将会提前处理 var、函数声明、class、const 和 let 这些语句,以确定其中变量的意义,但因为一些历史包袱这部分内容非常复杂。

1. var 声明

var 声明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值部分,只管在当前作用域声明这个变量。

var a = 1;

function foo() {
    console.log(a);
    var a = 2;
}

foo();

以上代码声明了一个脚本级别的 a,又声明了 foo 函数体级别的 a,而函数体级别的 var 出现在 console.log 语句之后。但是,预处理过程在执行之前,所以有函数体级的变量 a,就不会去访问外层作用域中的变量 a 了,而函数体级别的变量 a 此时还没有赋值,所以是 undefined。

var a = 1;

function foo() {
    console.log(a);
    if(false) {  // 增加了这行
        var a = 2;
    }
}

foo();

我们知道 if(false) 中的代码永远不会被执行,但是预处理阶段并不管这个,var 的作用能够穿透一切语句结构,它只认脚本、模块和函数体三种语法结构。所以这里结果跟前一段代码完全一样,我们会得到 undefined。

// 立即执行的函数表达式 IIFE
for(var i = 0; i < 20; i ++) {
    void function(i){
        var div = document.createElement("div");
        div.innerHTML = i;
        div.onclick = function(){
            console.log(i);
        }
        document.body.appendChild(div);
    }(i);
}
// 不用 IIFE 时,会得到 i===20
for(var i = 0; i < 20; i ++) {
    var div = document.createElement("div");
    div.innerHTML = i;
    div.onclick = function(){
        console.log(i);
    }
    document.body.appendChild(div);
}
// 当然,现在用 let 关键字就能解决
for (let i = 0; i < 20; i++) {
    let div = document.createElement("div");
    div.innerHTML = i;
    div.onclick = function () {
        console.log(i);
    }
    document.body.appendChild(div);
}

2. let 和 const 声明

let 和 const 声明也会被预处理。如果当前作用域内有声明,就无法访问到外部的变量。

const a = 2;
if(true){
    console.log(a); //抛错
    const a = 1;   
} // 在 if 的作用域,const 声明仍然是有预处理机制

2. function 声明

function 声明的行为原本跟 var 非常相似,但是在最新的 JavaScript 标准中,对它进行了一定的修改,这让情况变得更加复杂了。

在全局(脚本、模块和函数体),function 声明不但在作用域中加入变量,还会给它赋值

console.log(foo); // 有值了
function foo(){
}

但 function 声明是在 if 等创建的块级作用域时,预处理只会有变量,但不会提前赋值。

console.log(foo);
if(true) { // 如果出现在 if 等语句中时,
    function foo(){

    }
}

但在 if 创建的块级作用域中,变量是会被提前,且产生赋值效果。

3. class 声明

class 声明在全局的行为跟 function 和 var 都不一样。

// 在声明前使用,会报错
console.log(c);
class c{

}
var c = 1;
function foo(){
    console.log(c); // ReferenceError: Cannot access 'c' before initialization
    class c {}
}
foo();

以上代码执行后,仍然会报错。

如果去掉第四行的 class 声明,则会正常打印出 1,这说明后面出现的 class 声明影响了前面语句的结果。也就是说 class 声明也是会被预处理的,它会在作用域中创建变量,并且要求在定义前访问它时会抛出错误。

class 的声明作用不会穿透 if 等语句结构,所以只有写在全局环境才会有声明作用。class 声明的特征跟 const 和 let 类似,都是作用于块级作用域,预处理阶段则会屏蔽外部变量。

这样的 class 设计比 function 和 var 更符合直觉,而且在遇到一些比较奇怪的用法时,倾向于抛出错误。按照现代语言设计的评价标准,及早抛错是好事,它能够帮助我们尽量在开发阶段就发现代码的可能问题。

此外,class 默认内部的函数定义都是 strict 模式的。

Last updated