理解闭包前必会的——js作用域

115 阅读8分钟

前言

JavaScript(JS)基础对于程序员来说非常重要,尤其是对于前端开发者和全栈开发者。对JS的基础知识的深入理解可以帮助你写出更快、更高效、更少出错的代码。在面试中,经常会现的闭包,对于初学者来说过于抽象。要想学会理解闭包,有一个非常重要的知识点要掌握——————作用域和作用域链

1.作用域

几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问和修改。而这些变量储存在哪里?最重要的是,程序需要是如何找到他们?这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量。这套规则被称为作用域。

Js中包含着三种作用域

1. 全局作用域
2. 函数作用域
3. 块级作用域

1.1全局作用域

在函数外部声明的变量拥有全局作用域。这意味着在代码的任何地方都可以访问这些变量。

     var globalVariable = "我是全局变量"; 
    function exampleFunction() { 
        console.log(globalVariable); // 输出: "我是全局变量" 
    }         
    exampleFunction();

1.2函数作用域

当在函数内部声明一个变量时,该变量只能在这个函数内部被访问,它是局部作用域或函数作用域的一部分。

function foo(a){
    var b =2 
    function bar(){
    }
    var c = 3
}
bar() // 失败 ReferenceError错误
console.log(a,b,c) //三个都失败  ReferenceError错误

在这个代码片段中,函数foo(...)的作用域中包含了标识符a、b、c、和bar。因此无法从foo(...)的外部对它们进行访问,也就是说,这些标识符都无法从全局作用域中进行访问。也就是说,这些标识符全都无法从全局作用域中进行访问,因此会导致ReferenceError错误。

1.3块级作用域

使用letconst关键字声明的变量在ES6及以后的版本中具有块级作用域。这意味着它们只能在声明它们的块(例如,if语句或for循环)中被访问。

var foo = true;
if(foo) {
    let bar = foo * 2
    bar = something(bar)
    console.log(bar)
}
console.log(bar)  //ReferenceError

let + {} 可以形成块级作用域,上述代码中在外部作用域中无法调用声明在块级作用域中的bar,因此最后输出的结果会发生ReferenceError错误。

2.作用域链

在JavaScript中,作用域链是一个非常核心的概念,它与变量的查找和访问机制密切相关。 作用域链的基本思想是:当代码中的一个变量被访问时,JavaScript会首先在当前作用域中查找这个变量。如果没有找到,它会继续在外层的作用域中查找,直到找到该变量或者到达全局作用域为止。

    var global = "我是全局变量"
    function outerFoo(){
        var outer = "我是外层函数变量"
        function innerFoo(){
            var inner = "我是内层函数变量"
            console.log(global) // 我是全局变量
        }
    }

在上述代码中,innerFoo()有着自己的一个局部作用域,它还可以访问outerFoo()的作用域和global所在的全局作用域。这三种作用域,形成了作用域链。

总的来说,作用域链是确保JavaScript引擎正确查找变量的机制,而这个查找过程是从当前的执行上下文开始,然后逐级向外部作用域查找,直到全局作用域。/p>

3词法作用域

简单的说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况是这样的)

    function foo(a){
        var b = a * 2
        function bar(c){
            console.log(a,b,c)
        }
        bar(b * 3)
    }
    foo(2) // 2, 4, 12

在这个例子中有三个逐级嵌套的作用域。为了帮助理解,可以将他们想象成几个逐级包含的气泡

image.png

- 泡泡1包含着整个全局作用域,其中只有一个标识符:foo。
- 泡泡2包含着foo所创建的作用域,其中有三个标识符:a,bar,b。
- 泡泡3包含着bar所创建的作用域,其中只有一个标识符:c。

作用域气泡由其对应的作用域块代码写在哪里决定,他们是逐级包含的。无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由其被声明时所处的位置决定

3.1欺骗词法

欺骗词法作用域会导致性能下降,在详细解释性能问题之前,先来看看这两种机制分别是什么原理。

3.1.1 eval()

JS中的eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写中就存在于程序中这个位置的代码。根据这个原理来理解eval(..)是如何通过代码欺骗和假装书写时代码就在那,来实现修改词法作用域环境的。

在执行eval(..)之后的代码时,引擎并不知道或在意前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改。引擎只会如往常地进行词法作用域查找。

   function foo(str,a){
       eval(str) //欺骗
       console.log(a,b)
   }
   var b = 2
   foo("var b = 3",1)  //1,3

eval(..)调用中的"var b = 3"这段代码会被当做原本就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(..)的词法作用域进行了修改。当console.log(a,b)被执行时,会在foo(..)内部同时找到a和b,但双永远也无法找到外部的b。因此会输出“1,3”。

3.1.2 with()

with通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

   var obj = {
       a:1,
       b:2,
       c:3
   }
   //单调乏味的重复"obj"
   obj.a = 2
   obj.b = 3
   obj.c = 4
   
   //简单的快捷方式
   with(obj){
   a = 3
   b = 4
   c = 5
   }

但实际上不仅仅是为了方便地访问对象属性。考虑如下代码:

   function foo(obj){
       with(obj){
           a = 2
       }
   }
   var o1 = {
        a = 3
   }
   var o2 = {
        b = 3
   }
   foo(o1)
   console.log(o1.a)  //2
   
   foo(o2)
   console.log(o2.a)  // undefined
   console.log(a)   //2   ————a被泄露到全局作用域上了!

这个例子中创建了o1和o2两个对象。其中一个具有a属性,另外一个没有。foo(..)函数接受一个obj参数,该参数是一个对象的引用,并对这个对象引用执行了with(obj){..}。在with块内部,我们写的代码看起来只是对变量a进行简单的赋值。但实际上就是声明了一个变量a,并将2赋值给它。 但是可以注意到有一个奇怪的副作用,实际上a = 2 赋值操作创建了一个全局的变量a。

3.1.3性能

JS引擎会在编译阶段进行数项的性能优化,其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速的找到标识符。但如果引擎在代码中发现了eval()或with,它只简单的假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道eval()会接收到什么代码,这些代码如何对作用域进行修改,也无法知道传递给with用来创建新词法作用域的对象的内容到底是什么

如果代码中大量使用eval()或with,那么运行起来一定会变得非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行得更慢的事实。

总结

当我们讨论JavaScript的作用域时,我们实际上是在讨论一个变量在何处可以被访问。以下是关于JavaScript作用域的总结:

  1. 词法作用域(静态作用域) :

    • JavaScript使用的是词法作用域,也被称为静态作用域。
    • 词法作用域是在代码书写的时候就确定了的,而不是在运行时。这意味着变量的作用域是由它在源代码中的位置来决定的。
  2. 全局作用域:

    • 在所有函数之外声明的变量处于全局作用域。
    • 全局变量可以在代码的任何地方被访问。
  3. 局部作用域(函数作用域) :

    • 在函数内部声明的变量具有局部作用域。
    • 这意味着它们只能在该函数内部被访问和修改。
  4. 块级作用域:

    • 使用letconst关键字声明的变量在ES6及以后的版本中具有块级作用域。
    • 这意味着变量只能在声明它们的块(如if语句或for循环)中被访问。
  5. 作用域链:

    • 当在一个函数内部访问一个变量时,JavaScript会首先在当前作用域查找。如果没有找到,它会继续在上一级的外层作用域查找,直到达到全局作用域。
    • 这个查找过程形成了所谓的作用域链。