likes
comments
collection
share

深入浅出 JavaScript作用域

作者站长头像
站长
· 阅读数 19

前言

JavaScript的执行过程始于解析,将源代码转换成抽象语法树,随后进行预编译,包括变量和函数声明提升。执行阶段逐行执行代码,可能涉及函数调用、条件语句等操作,并在运行时动态解析和执行代码。最后,一些引擎会通过优化技术,如JIT编译等,提高代码性能,包括内联缓存、去除未使用代码等。这些步骤相互配合,确保JavaScript代码能够高效地执行。

编译过程

编译过程是将源代码转换为目标代码的过程。在编译过程中,通常包括词法分析、语法分析、语义分析、优化和代码生成。

举个例子:

let x = 5;
let y = x + 3;
  1. 在词法分析阶段,会识别出letxy=5+3等词法单元。

  2. 在语法分析阶段,会构建出相应的语法树,确定变量声明和表达式的结构。

  3. 在语义分析阶段,会检查变量的使用是否合理,以及表达式的计算是否有意义等。

  4. 在代码生成阶段,会生成实际的可执行代码。

    //最后生成
    let x = 5;
    let y = x + 3;
    

作用域

作用域是指变量、函数等在代码中可被访问的范围。它决定了变量和函数在何处以及如何被查找和使用。

简单来说,作用域规定了哪些代码可以访问特定的变量或执行特定的函数。

作用域的作用

  1. 避免命名冲突:确保不同作用域中的变量和函数具有唯一的标识符,防止变量被意外覆盖或错误使用。
  2. 代码组织和维护:明确变量的有效范围,使代码结构更清晰,便于理解和维护。
  3. 实现封装性:将相关的代码和变量封装在特定作用域内,实现一定程度的隐藏和保护。
  4. 控制变量的生命周期:不同作用域中的变量在不同时间被创建和销毁,作用域控制有助于管理变量的存在时间。

有效标识符

有效标识符是在程序中,用户自定义的用来命名变量、函数、类、模块等的字符序列,它需满足以下条件:

  1. 由字母、数字、下划线(_)或美元符号($)组成。
  2. 不能以数字开头。
  3. 通常具有一定的语义,能直观地表达其代表的意义。

作用域的分类

作用域可以分为函数作用域,全局作用域和块级作用域。

  1. 全局作用域:在程序的最外层定义的变量拥有全局作用域,在整个程序中都可以访问。
  2. 函数作用域:在函数内部定义的变量属于函数作用域,只能在函数内部及嵌套在函数内的子作用域中被访问。
  3. 块级作用域:如用花括号括起来的代码块内定义的变量,具有块级作用域。

首先我们用例子开始。

eg1

var a = 1
console.log(a)
//输出:1

eg2

function b(){
    var a = 1
}
b()
console.log(a)
//报错:a is not defined

eg3

 var a = 1
function b(){
   console.log(a)
}
b()
//输出:1

在eg2代码从上往下执行,运行了b()函数并定义了a=1,但是输出a会报错。但是eg1不报错。

在eg2中,a是在函数中定义的,而在eg1中,a是在全局中定义的。在eg2中,a是b函数的有效标识符,是在函数作用域中;在eg1中,a是在全局作用域中。我们可以从eg1和eg2中看出,全局作用域是无法访问内部作用域

用eg2和eg3进行对比,我们可以看出,函数访问了全局变量。我们可以得出,内部作用域可以访问全局作用域

作用域的规则:外层作用域是无法访问内部作用域,内部作用域可以访问外层作用域,最大的外层作用域是全局作用域。

这主要是由作用域的工作机制决定的。

当代码在执行时,内层作用域是在外层作用域的基础上创建的。内层作用域可以访问外层作用域中的变量和函数,这被称为“作用域链”。

而外层作用域无法直接访问内层作用域中的变量,这是为了避免变量冲突和混乱,保持代码的清晰性和可维护性。如果允许外层作用域随意访问内层作用域,可能会导致意外的结果和难以调试的问题。

eg4

if(true){
    var a = 1
}
console.log(a)
//输出:1

eg5

if(true){
    let a = 1
}
console.log(a)
//报错:a is not defined

我们用eg4和eg5进行对比,我们会发现区别就是var和let。

在eg5中在全局作用域中找不到变量,所有就一定存在一个作用域,但是里面并没有函数,所以不是函数作用域。那是什么作用域你?

是块级作用域。{let……}或{const……}可以构成一个块级作用域。

声明关键词

声明关键词由var、let和const。

  1. let和var的区别

    • var存在声明提升,let不存在。

      console.log(a)
      var a = 1
      //返回:undefined
      
      //声明提升后是这样的效果
      var a
      console.log(a)
      a=1
      
      console.log(a);
      let a = 1
      // 报错:Cannot access 'a' before initialization at Object
      
    • let会和{}形成块级作用域。

    • var可以重复声明变量let不可以(会报错)。

      var a = 1
      var a = 2
      console.log(a)
      //输出:2
      

      深入浅出 JavaScript作用域

      let重复声明会出现提示。

    • let存在暂时性死区。暂时性死区是指在变量使用之前,从该变量的作用域开始到该变量声明的位置之间的区域。

      let a = 1
      
      if (1) {
          console.log(a);   //暂时性死区
          let a = 2
      }
      //报错:Cannot access 'a' before initialization at Object.
      

      在这个例子中,console.log(a) 这一行会报错。

      这是因为在 if 块内部,又使用 let 重新声明了变量 a,在这个重新声明之前的区域就是暂时性死区,不能访问之前的变量 a

  2. let和const的区别

    • let可以重新赋值

      let num = 5;
      num = 10; 
      console.log(num);
      //输出10
      
    • const声明后不能重新赋值

      const pi = 3.14
      pi = 3.15;
      console.log(pi);
      //报错:Assignment to constant variable.
      

欺骗词法作用域

欺骗词法作用域(也称为打破词法作用域)是指通过一些特殊的手段或技术,在某种程度上绕过或改变原本的词法作用域规则。

  1. eval()将原本不属于这里的代码变成就像天生就定义在了这里一样。

    function foo(str, a) {
        eval(str)    //var b=3
        console.log(a, b);   //1,3
    }
    
    var b = 2
    foo('var b = 3', 1)
    
  2. with()用于修改一个对象的属性值,但如果修改的属性在原对象中不存在,那么该属性就会被泄漏到全局。

    function foo(obj) {
        with (obj) {
            a = 2
        }
    }
    
    var ol1 = {
        a: 3
    }
    
    var ol2 = {
        b: 3
    }
    
    foo(ol2)
    console.log(ol2);   //输出:{b:3}
    console.log(a);    //输出:2
    

小结

在这篇文章里,我们提到了词法分析、有效标识符、作用域、声明关键词、声明关键词之间的区别和欺骗词法作用域。

转载自:https://juejin.cn/post/7361852837267980315
评论
请登录