JavaScript 变量的秘密,你知道吗

979 阅读12分钟

在变量中存储值是编程中的一个基本概念。变量的“范围”决定了它在整个程序中何时可用和不可用。理解 JavaScript 中的变量作用域是在语言中打下坚实基础的关键之一。

本文将解释 JavaScript 的作用域系统是如何工作的。您将了解声明变量的不同方式、局部作用域和全局作用域之间的区别,以及称为“提升”的东西——一种 JavaScript 怪癖,可以将看似无辜的变量声明变成一个微妙的错误。

变量范围

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

目前,在 JavaScript 中声明变量的方法有三种:使用 oldvar关键字,以及使用 newlet和const关键字。在 ES6 之前,使用var关键字是声明变量的唯一方法,但现在我们可以使用letand const,它有更严格的规则,并且代码更不容易出错。我们将在下面探讨所有三个关键字之间的差异。

范围规则因人而异。JavaScript 有两个作用域:globallocal。本地作用域有两种变体:旧的函数作用域和ES6 引入的新块作用域。 值得注意的是,函数作用域实际上是块作用域的一种特殊类型。

全球范围

在脚本中,最外层的作用域是全局作用域。在此范围内声明的任何变量都将成为全局变量,并且可以从程序中的任何位置访问:

// Global Scope

const name = "Monique";

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

sayHi();
// Hi Monique

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

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

本地范围

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

varJavaScript 中的函数定义了使用,let和声明的变量的范围const。在该函数中声明的任何变量只能从该函数和任何嵌套函数中访问。

代码块(if,for等)仅为使用letandconst关键字声明的变量定义范围。该var关键字仅限于函数作用域,这意味着只能在函数内部创建新作用域。

letandconst关键字具有块作用域,它为声明它们的任何块创建一个新的本地作用域。您还可以在 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();

在这里,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');   

在这个例子中,我们可以看到用 and 声明的andgreeting变量name在块外是不可访问的。constletif

现在让我们替换andconst看看会发生什么:letvar

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');

如您所见,当我们使用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

即使名称相同,局部变量在函数执行后也不会覆盖全局变量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)

这一次,局部变量test覆盖了同名的全局变量。当我们在testScope()函数内部运行代码时,全局变量被重新分配。如果一个局部变量在没有首先用关键字声明的情况下被赋值var,它就变成了一个全局变量。为避免此类不良行为,您应始终在使用局部变量之前声明它们。在函数中使用关键字声明的任何变量var都是局部变量。声明变量被认为是最佳实践。

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

吊装

JavaScript 解释器在幕后执行许多操作,其中之一就是“提升”。如果您不知道这种“隐藏”行为,可能会引起很多混乱。考虑 JavaScript 变量行为的最佳方式是始终将它们可视化为由两部分组成:声明和初始化/赋值:

var state;             // variable declaration
state = "ready";       // variable assignment

var state = "ready";   // declaration plus assignment

在上面的代码中,我们首先声明了变量state,然后我们给它赋值"ready"。而在最后一行代码中,我们看到这两个步骤可以合并。但是您需要记住的是,即使它们看起来像一个语句,实际上 JavaScript 引擎也会将该单个语句视为两个单独的语句,就像示例的前两行一样。

我们已经知道在一个范围内声明的任何变量都属于该范围。但我们还不知道的是,无论变量在特定范围内声明的位置,所有变量声明都会移动到其范围的顶部(全局或局部)。这称为提升,因为变量声明被提升到范围的顶部。请注意,提升只会移动声明。任何分配都留在原地。让我们看一个例子:

console.log(state);   // output: undefined
var state = "ready";

如您所见,当我们记录 的值时state,输出为undefined,因为我们在实际赋值之前引用了它。您可能期望 aReferenceError被抛出,因为state尚未声明。但你不知道的是,变量undefined在幕后用默认值声明和初始化的。以下是 JavaScript 引擎解释代码的方式:

var state;           // moved to the top
console.log(state);   
state = "ready";     // left in place

重要的是要注意变量没有物理移动。吊装只是一个模型,描述了 JS 引擎在幕后所做的事情。

现在,让我们看看提升是如何与let变量一起工作的:

{
  // Temporal dead one (TDZ) starts at the beginning of the scope
  console.log(state);   // output: "ReferenceError: Cannot access 'state' before initialization
  let state = "ready";  // end of TDZ. TDZ ends at actual variable declaration
}   

在此示例中,控制台输出不是undefined,但会引发引用错误。为什么?let与变量相比,var变量在完全初始化之前无法读取/写入。它们仅在代码中实际声明的地方才被完全初始化。因此,let变量声明被提升但未使用undefined值初始化,变量就是这种情况var。从块开始到实际变量声明的部分称为Temporal Dead Zone。这是一种确保更好的编码实践的机制,强制您在使用变量之前声明它。如果我们将控制台语句移出 TDZ,我们将得到预期的输出:ready.

{
  // Temporal dead one (TDZ) starts at the beginning of the scope
  let state = "ready";  // end of TDZ. TDZ ends at actual variable declaration
  console.log(state);   // output: ready
} 

用关键字声明的变量与变量const具有相同的行为let。

职能

提升也会影响函数声明。但在我们看一些例子之前,让我们先了解一下函数声明和函数表达式之间的区别:

function showState() {}          // function declaration
var showState = function() {};   // function expression

区分函数声明和函数表达式的最简单方法是检查单词function在语句中的位置。如果function是语句中的第一件事,那么它就是一个函数声明。否则,它是一个函数表达式。

函数声明被完全提升。这意味着整个函数的主体被移动到顶部。这允许您在声明函数之前调用它:

showState();            // output: Ready

function showState() {
  console.log("Ready");
} 

var showState = function() {
  console.log("Idle");
};

上述代码有效的原因是 JavaScript 引擎将showState()函数的声明及其所有内容移到了作用域的开头。代码解释如下:

function showState() {     // moved to the top (function declaration)
  console.log("Ready");
} 

var showState;            // moved to the top (variable declaration)

showState();  

showState = function() {   // left in place (variable assignment)
  console.log("Idle");
};

您可能已经注意到,只有函数声明被提升,但函数表达式没有。将函数分配给变量时,规则与变量提升的规则相同(仅移动声明,而分配保留在原地)。

在上面的代码中,我们看到函数声明优先于变量声明。在下一个示例中,我们将看到,当我们有一个函数声明与一个变量赋值时,最后一个优先:

var showState = function() {
  console.log("Idle");
};

function showState() {
  console.log("Ready");
} 

showState();            // output: Idle

这一次,我们showState()在代码的最后一行调用了函数,这改变了情况。现在我们得到输出"Idle"。下面是它在被 JavaScript 引擎解释时的样子:

function showState(){        // moved to the top (function declaration)
  console.log("Ready");
} 

var showState;               // moved to the top (variable declaration)

showState = function(){      // left in place (variable assignment)
  console.log("Idle");
};

showState();

注意:箭头函数与函数表达式的工作方式相同。

课程

类声明也以与用let语句声明的变量类似的方式提升:

// Using the Person class before declaration
var user = new Person('David', 33); // output: ReferenceError: Cannot access 'Person' before initialization

// Class declaration
class Person {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
}

在这个例子中,我们可以看到Person在声明之前使用类会产生类似于在let变量中的引用错误。为了解决这个问题,我们必须Person在声明之后使用类:

// Class declaration
class Person {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
}

// Using the Person class after declaration
var user = new Person('David', 33);
console.log(user); 

也可以使用类表达式、 usingvar或let变量const声明语句来创建类:

// Using the Person class
console.log(typeof Person);   // output: undefined

var user = new Person('David', 33); // output: TypeError: Person is not a constructor

// Class declaration using variable statement
var Person = class {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
};

在这个例子中,我们可以看到这个Person类被提升为一个函数表达式,但它不能被使用,因为它的值是undefined. 同样,为了解决这个问题,我们必须Person在声明之后使用类:

// Using the Person class
console.log(typeof Person); // output: undefined

// Class declaration using variable statement
var Person = class {
  constructor(name, age) {
    this.name = name; 
    this.age = age;
  }
};

// Using the Person class after declaration
var user = new Person('David', 33);
console.log(user);  

要记住的事情

  • var变量是函数范围的。
  • let并且const变量是块范围的(这也包括函数)。
  • 在执行代码的任何部分之前,所有声明(类、函数和变量)都被提升到包含范围的顶部。
  • 首先提升函数,然后提升变量。
  • 函数声明优先于变量声明,但不高于变量赋值。

如果本文对你有帮助,别忘记给我个3连问 ,点赞,转发,评论,,咱们下期见。

收藏 等于白嫖,点赞才是真情。

亲爱的小伙伴们,有需要JAVA面试文档资料请点赞+转发,关注我后,私信我333就可以领取免费资料哦

\

JavaScript 变量的秘密,你知道吗