javascript中的作用域

前言

  本篇是基于对 《你不知道的JavaScript(上卷)》中的第一、二、三、四章的总结理解。

编译原理

  在代码执行之前进行的操作叫“编译”,一般有三个步骤:

  1. 分词/词法分析:这个操作是将有意义的代码生成词法单元;
  2. 解析/语法分析:将词法单元生成为AST(抽象语法树);
  3. 代码生成:将AST转换为可执行代码。

  但对于JavaScript来说,编译过程更加复杂一些,会对运行性能进行优化,以及对冗余的元素进行优化,因为 js 是动态语言,编译操作在代码运行的几微秒前,编译后马上执行。

 

执行原理(宏观)

  JS 执行代码大概涉及到三个东西,分别是,编译器(进行编译)、引擎(执行)、作用域(维护变量作用域)。

  • 例1:

    假设有 var a = 1 这段代码,编译器、引擎、作用域之间的协同工作是:

      首先编译器进行编译,编译器会检查当前作用域下是否存在相同的变量a,如果存在就忽略(注意,let、const声明会报错),不存在就会在当前作用域中添加一个新的变量,命名为a。最后生成引擎运行时需要的代码。

      最后引擎执行时会首先检查变量a是否存在在当前作用域中(不存在会向外层作用域查找),如果直到全局作用域都没有变量a,则会抛出一个异常,如果存在,就处理 a = 2 这个赋值操作。

  • 例2:

    假设有var a = 1; var b = a; :

      这里编译过程跟例1相同,需要注意 var b = a,这里引擎会先在作用域中查找a的值是什么(没找到就报错),查找b是否存在在作用域中(没找到报错),最后进行赋值。

  

  在编译器或引擎查找某个变量是否存在作用域中叫“LHS”查找;而查找某个变量的值是什么叫 “RHS” 查找。

  在当前作用域进行查找时,如果没有找到某个变量,则会向外层作用域进行查找。

作用域

  js中,作用域一般分为:全局作用域和函数作用域。没有块作用域的概念。如下:

if(1) {
        var a = 1
}

console.log(a)  // 1

  a 仍然输出为1。在其他语言中可能会报错,因为 a 在 if 的块作用域中。而在js中仍然是全局作用域。

 

var a = 2

function fun(){
        var a = 1

        function fun2() {
            console.log(a) // 1
        }

        fun2()

}

fun()

  在函数(fun)作用域内声明了 a ,查找过程中没有在fun2当前作用域内找到,所以到上层作用域(fun)内查找,有 a,所以打印出 1。此时没有往全局作用域内查找。  

 

  • JS使用块作用域

    在ES6出现之前,想要使用块作用域,只能使用with 或者 try/catch。

try{
        throw 2
    } catch(a){
        console.log(a) // 2
    }

console.log(a) // 报错

    说明 catch 中是有块作用域的。

    也可以使用let、const将变量绑定在当前块作用域中,但需要支持ES6。

  

  • 欺骗作用域

var a = 1

function fun(e) {
  console.log(a)  // 2
}

fun(eval("var a = 2"))

  上面代码打印出的是 2 ,并不是1,是因为使用了eval,就好像在当前作用域中声明了一个跟全局作用域中 a 相同的变量,按照查找规则,就在当前作用域中找到了 a,所以打印出 2,上面代码相当于:

var a = 1

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

fun()

  当然,这在js非严格模式下有用。

   

  • IIFE:立即执行函数表达式

  普通的定义一个函数,它的函数名本身就“污染”了作用域。正如一些类库,都会使用 IIFE 来避免“污染”。

(function IIFE(){
        
})()

  函数名字不是必须的,可以是匿名函数。

  函数定义外层有一对括号,加了括号就变成了表达式。

  括号将函数的作用域绑定在了表达式自身,从而不会“污染”外层作用域。

  IIFE 还可以传参,在函数执行中传入参数。

(function IIFE(a){
    console.log(a) // 1
})(1)

 

 

  • 作用域中的声明提升

  代码在执行前会对声明在 当前作用域内 进行提升:

// 提升前代码
    var a = 1
    console.log(a)

    // 提升后代码
    var a
    a = 1
    console.log(a)

    /////////////////////////////////////

// 提升前代码 a () function a () { console.log(1) } // 提升后代码 function a () { console.log(1) } a ()

  因为声明经过提升,所以函数a定义在函数执行后才能正确执行。

 

// 提升前
fun()
function fun () {
        console.log(a) // undefined
        var a = 1
}

// 提升后
function fun () {
        var a
        console.log(a) // undefined
        a = 1
}
fun()

  上面代码,虽然变量a经过提升,但是在打印前并没有经过赋值,所以打印出 undefined。

 

// 提升前
fun() // 报错
var fun = function () {}

// 提升后
var fun
fun()
fun = function () {}

  上面代码执行报错,因为var fun = function 是表达式,所以只会提升var fun声明。

 

  另,变量与函数同时提升,那么会先提升函数:

// 提升前
var a = 1
function fun(){}

// 提升后
function fun(){}
var a
a = 1

 

  

 

posted @ 2021-02-25 18:57  blogCblog  阅读(108)  评论(0编辑  收藏  举报