JavaScript的作用域和预编译

170 阅读5分钟

前言

我们在学习JavaScript时往往会漏掉许多基础的细节,就去学习更加深层的东西,在系统报错时就初略地改下代码而并不知道为什么这里会报错。我将在这篇文章告诉大家var let const这些关键词的区别,还有作用域以及预编译方面的细节,平时常出现的问题。对于每个方面的阐述我都会给出相应的例子,以供大家更好地理解。

我们先从作用域入手,再讲关键词,将较难的预编译放到最后。

作用域

作用域分为全局作用域函数作用域块级作用域词法作用域,先分别介绍一下:

  • 全局作用域:最大的作用域级别,定义在全局作用域中的变量和函数可以在代码的任何地方被访问。
  • 函数作用域:也称为局部作用域,顾名思义,就是在一个函数内部定义的变量的作用域,当函数被调用时,一个新的函数作用域被创建,当函数执行完毕时,这个作用域也随之销毁。
  • 块级作用域:使用let和const声明的变量具有块级作用域,它们只能在它们声明的代码块中有效,代码块是由花括号括起来的那块代码,如if语句、for循环中都算一个代码块。
  • 词法作用域:变量声明的地方,也就是变量声明时所处的作用域。
var a = 1                                
function foo(){     //
     var a = 2      //函数作用域
     }              //
foo()
console.log(a);  //输出 1

其中函数体内的便称为函数作用域,而整一块就是全局作用域

怎么理解作用域呢作用域就是对象所能访问到的区域,内部作用域可以访问外部作用域,反之则不行。所以全局作用域中的a访问不到函数作用域,便输出1。内部作用域和外部作用域就像儿子与父亲一样,儿子向爸爸要东西很正常,但爸爸向儿子要东西就不应该了。

var vs let vs const

为什么先讲作用域呢,就是为了让我们更好地了解这些关键词的区别,毕竟它们的主要区别就在这里。

  • var具有函数作用域,并且在同一个函数内可以多次声明同名变量不会报错。声明提升现象,即在声明之前就可以访问到变量,但其初始值为undefined。
console.log(a); //输出undefined
var a = 1;
//上面的代码可看作为
var a;
console.log(a);
a = 1;           //这就是所谓的声明提升
  • let具有块级作用域、不具有声明提升、不允许重复声明。
  • const具有块级作用域、不具有声明提升、不允许重复声明。

预编译

我们先看下面的代码:

function fn(a) {
    console.log(a);   // 1
    var a = 123
    console.log(a);   // 123
    function a(){}    //
    console.log(a);   // function a(){}
    var b = function () {}  
    console.log(b);   // function b(){}
    function d() {}   //
    var d = a         //
    console.log(d);   // function a(){}
  }
  fn(1)

如果你觉得上面注释的是正确的输出结果,那么现实很残酷,其实上面的答案只有一个是正确的,正确答案将在最后揭晓

先开始我们的预编译学习,预编译分为 全局的预编译函数中的预编译,先以专业的口吻来给大家介绍它们的步骤:

  • 全局的预编译
  1. 创建 全局执行上下文对象 (称为GO)
  2. 找变量声明,变量名作为GO的属性名,值为undefined
  3. 在全局找函数声明,函数名作为GO的属性名,值为函数体
  • 函数中的预编译
  1. 创建 函数的执行上下文对象 (称为AO)
  2. 找形参和变量声明,将形参和变量名作为AO的属性,值为undefined
  3. 将实参和形参统一
  4. 在函数体内找函数声明,将函数名作为AO的属性名,值赋予函数体

两者的过程非常相似,我们还是以刚刚列举的代码为例子:

function fn(a) {             
    console.log(a);           
    var a = 123                
    console.log(a);
    function a(){}            //函数声明
    console.log(a);  
    var b = function () {}    //函数表达式
    console.log(b);   
    function d() {}           //函数声明
    var d = a         
    console.log(d);   
  }
  fn(1)

先对全局预编译,遵循上面的步骤:

  1. 创建GO --> GO{}
  2. 没有变量声明
  3. 添加函数声明 -->GO{ fn:function(){} }

完成了全局编译后便开始执行,function fn(a) {...}跳过,执行fn(1),这时就会进行对fn函数的预编译

  1. 创建AO --> AO{}
  2. 将形参和变量名作为AO的属性,值为undefined --> AO{a:undefined ,b:undefined ,d:undefined }
  3. 将实参和形参统一 --> AO{a:undefined->1 ,b:undefined , d:undefined }
  4. 找函数声明,将函数名作为AO的属性名 --> AO{a:undefined->1->function a(){} ,b:undefined ,d:function d(){} }

其中后面的值将覆盖前面的值

预编译完成,就可以执行函数中代码了。

function fn(a) {
    console.log(a);   //输出AO中的a的值function a() {}
    var a = 123       //赋值 a = 123
    console.log(a);   //输出 123
    function a(){}    //函数声明
    console.log(a);   //输出123
    var b = function () {}   //函数表达式
    console.log(b);   //输出 function b() {}
    function d() {}   //函数声明
    var d = a         //赋值 d = 123
    console.log(d);   //输出 123
  }
  fn(1)

上面的注释就是输出的正确答案了,如果还是有疑问可以再结合预编译的步骤走一遍。

结语

这些就是我们在JavaScript上容易忽视或者忘记的细节,同时也是面试官在js方面可能会问到的问题,在之后我也会分享更多有关JavaScript方面的知识点,希望可以帮助到大家,感谢观看。