揭开JavaScript变量范围和提升的神秘面纱

289 阅读8分钟

Demystifying JavaScript Variable Scope and Hoisting

在变量中存储数值是编程中的一个基本概念。一个变量的 "范围 "决定了它在整个程序中何时可用,何时不能用。理解JavaScript中的变量范围是建立坚实语言基础的关键之一。

这篇文章将解释JavaScript的范围系统是如何工作的。你会了解到声明变量的不同方式,局部范围和全局范围的区别,以及一种叫做 "提升 "的东西--一种可以将看似无害的变量声明变成一个微妙的错误的JavaScript怪癖。

变量范围

在JavaScript中,变量的范围是由变量声明的位置控制的,它定义了程序中某个特定变量可以被访问的部分。

目前,有三种方法可以在JavaScript中声明一个变量:使用旧的var 关键字,以及使用新的letconst 关键字。在ES6之前,使用var 关键字是声明变量的唯一方法,但现在我们可以使用letconst ,这两个关键字有更严格的规则,使代码不容易出错。我们将在下面探讨这三个关键字之间的区别。

不同语言的作用域规则各不相同。JavaScript有两个作用域:全局局部。本地范围有两种变化:旧的函数范围,以及ES6引入的新的块范围。值得注意的是,函数作用域实际上是块作用域的一种特殊类型。

全局作用域

在一个脚本中,最外层的作用域是全局作用域。在这个作用域中声明的任何变量都会成为全局变量,可以从程序的任何地方访问。

// Global Scope

const name = "Monique";

function sayHi() {
  console.log(`Hi ${name}`);
}

sayHi();
// Hi Monique

正如这个简单的例子所示,变量name 是全局的。它被定义在全局范围内,可以在整个程序中被访问。

但是,尽管这看起来很方便,在JavaScript中不鼓励使用全局变量。例如,这是因为它们有可能被其他脚本或程序中的其他地方所覆盖。

本地范围

任何在一个块内声明的变量都属于该块,并成为局部变量。

JavaScript中的一个函数为使用var,letconst 声明的变量定义了一个范围。在该函数中声明的任何变量只能从该函数和任何嵌套函数中访问。

一个代码块(if,for, 等等)只为用letconst 关键字声明的变量定义一个范围。var 关键字被限制在函数范围内,这意味着新的范围只能在函数内部创建。

letconst 关键字有块作用域,它为任何声明它们的块创建一个新的、本地的作用域。你也可以在JavaScript中定义独立的代码块,它们同样可以划定一个作用域。

{
  // standalone block scope
}

函数和块的作用域可以被嵌套。在这种情况下,由于有多个嵌套的作用域,一个变量可以在它自己的作用域内或从内部作用域中访问。但是在它的作用域之外,该变量是不可访问的。

一个简单的例子来帮助直观地了解作用域

为了使事情更清楚,让我们用一个简单的比喻。我们世界上的每个国家都有边界。这些边界内的一切都属于这个国家的范围。每个国家都有很多城市,每个城市都有自己的城市范围。国家和城市就像JavaScript函数或块。它们有自己的局部作用域。大陆的情况也是如此。虽然它们的面积很大,但它们也可以被定义为区域。

另一方面,世界上的海洋不能被定义为有本地范围,因为它们实际上包裹了所有的本地对象--大陆、国家和城市--因此,它们的范围被定义为全球范围。让我们在下一个例子中直观地看到这一点。

var locales = {
  europe: function() {          // The Europe continent's local scope
    var myFriend = "Monique";

    var france = function() {   // France country's local scope
      var paris = function() {  // The Paris city's local scope
        console.log(myFriend);  // output: Monique
      };

      paris();
    };

    france();
  }
};

locales.europe();

请看笔
变量范围。1 by SitePoint (@SitePoint)
onCodePen.

在这里,myFriend 变量可以从paris 函数中获得,因为它被定义在france 函数的外部范围中。如果我们把myFriend 变量和控制台语句对调,我们会得到ReferenceError: myFriend is not defined ,因为我们不能从外部范围到达内部范围。

现在我们明白了什么是局部和全局作用域,以及它们是如何被创建的,现在是时候学习JavaScript解释器如何使用它们来寻找一个特定的变量。

回到给定的比喻,假设我想找到我的一个朋友,她的名字是Monique。我知道她住在巴黎,所以我从那里开始搜索。当我在巴黎找不到她的时候,我再往上走一级,把搜索范围扩大到整个法国。但同样,她不在那里。接下来,我再次扩大搜索范围,再升一级。最后,我在意大利找到了她,在我们的例子中,意大利是欧洲的本地范围。

在前面的例子中,我的朋友Monique是由变量myFriend 。在最后一行,我们调用了europe() 函数,该函数又调用了france() ,最后当paris() 函数被调用时,搜索开始了。JavaScript解释器从当前执行的范围开始工作,一直到找到相关的变量。如果在任何作用域中没有找到该变量,就会抛出一个异常。

这种类型的查找被称为词法(静态)范围。程序的静态结构决定了变量的范围。变量的作用域由其在源代码中的位置决定,嵌套函数可以访问其外部作用域中声明的变量。无论一个函数从哪里调用,甚至如何调用,其词法范围只取决于函数的声明位置。

现在我们来看看新的块作用域是如何工作的。

function testScope(n) {
  if (true) {
    const greeting = 'Hello';
    let name = n;
    console.log(greeting + " " + name); // output: Hello [name]
  }
  console.log(greeting + " " + name); // output: ReferenceError: greeting is not defined
}

testScope('David');   

参见CodePen上的Pen
Variable Scope: 2 by SitePoint (@SitePoint)

在这个例子中,我们可以看到,用constlet 声明的greetingname 变量在if 块之外是不可访问的。

现在让我们用var 替换constlet ,看看会发生什么。

function testScope(n) {
  if (true) {
    var greeting = 'Hello';
    var name = n;
    console.log(greeting + " " + name); // output: Hello [name]
  }
  console.log(greeting + " " + name); // output: Hello [name]
}

testScope('David');

见笔
变量范围。3 by SitePoint (@SitePoint)
onCodePen.

正如你所看到的,当我们使用var 关键字时,变量在整个函数范围内都是可以到达的。

在JavaScript中,同名的变量可以在多层嵌套作用域中被指定。在这种情况下,局部变量比全局变量获得优先权。如果你声明了一个本地变量和一个全局变量的名字相同,那么当你在一个函数或块中使用它时,本地变量将优先。这种类型的行为被称为阴影。简单地说,内部变量对外部变量有阴影。

这正是JavaScript解释器在试图找到一个特定变量时使用的机制。它从当时正在执行的最内层的作用域开始,一直到找到第一个匹配的变量为止,不管外层是否有其他同名的变量。让我们看一个例子。

var test = "I'm global";

function testScope() {
  var test = "I'm local";

  console.log (test);     
}

testScope();           // output: I'm local

console.log(test);     // output: I'm global

请看笔
变量范围。4 by SitePoint (@SitePoint)
onCodePen.

即使名称相同,在执行testScope() 函数后,局部变量也不会覆盖全局变量。但情况并非总是如此。让我们来考虑一下这个问题。

var test = "I'm global";

function testScope() {
  test = "I'm local";

  console.log(test);     
}

console.log(test);     // output: I'm global

testScope();           // output: I'm local

console.log(test);     // output: I'm local (the global variable is reassigned)

见笔者
变量范围。5 by SitePoint (@SitePoint)
onCodePen.

这一次,本地变量test ,覆盖了同名的全局变量。当我们运行testScope() 函数内的代码时,全局变量被重新赋值。如果一个局部变量在没有首先用var 关键字声明的情况下被赋值,它就会变成一个全局变量。为了避免这种不必要的行为,你应该在使用本地变量之前声明它们。任何在函数中用var 关键字声明的变量都是一个局部变量。声明你的变量被认为是最好的做法。

注意:在严格模式下,如果你没有首先声明变量就给变量赋值,那是一个错误。

继续阅读SitePoint上的 "解密JavaScript变量范围和提升"。