# 作用域与作用域链

# 目录

# 什么是作用域?

JavaScript 的作用域说的是变量的可访问性和可见性。也就是说整个程序中哪些部分 可以访问这个变量,或者说这个变量都在哪些地方可见。

# 为什么作用域很重要?

  1. 「作用域最为重要的一点是安全」。变量只能在特定的区域内才能被访问,有了作用域我们就可以避免在程序其它位置意外对某个变量做出修改。

  2. 「作用域也会减轻命名的压力」。我们可以在不同的作用域下面定义相同的变量名。

# 作用域的类型

# 全局作用域

任何不在函数中或者大括号中声明的变量,都是在全局作用域下,「全局作用域」下声明的变量可以在程序的任意位置访问。例如:

var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// 打印 'Hello World!'
greet();

# 局部作用域或函数作用域

在函数内部声明的变量是在局部作用域内。只能从该函数内部访问它们,这意味着它们不能从外部代码访问。例如:

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错 Uncaught ReferenceError: greeting is not defined
console.log(greeting);

# 块级作用域

ES6 引入了 letconst 变量,与 var 变量不同,它们的作用域可以是最接近的花括号对。这意味着,不能从那对花括号外面访问它们。例如:

{
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// 打印 'English'
console.log(lang);
// 报错 Uncaught ReferenceError: greeting is not defined
console.log(greeting);

从上面代码可以看出,大括号中使用 var 声明的变量可以在块外部使用,即 var 变量不在块级作用域内。

# 作用域嵌套

就像 JavaScript 中的函数可以在一个函数内部声明另一个函数一样,一个作用域可以嵌套在另一个作用域内。例如:

var name = 'Peter';
function greet() {
  var greeting = 'Hello';
  {
    let lang = 'English';
    console.log(`${lang}: ${greeting} ${name}`);
  }
}
greet();

在这里,我们有3个作用域彼此嵌套。首先,块作用域(由于 let 变量而创建)嵌套在局部作用域或函数作用域内,而嵌套作用域又嵌套在全局作用域内。

# 词法作用域

词法作用域(也叫静态作用域)从字面意义上看是说作用域在词法化阶段(通常是编译阶段)确定而非执行阶段确定的。看例子:

let number = 42;
function printNumber() {
  console.log(number);
}
function log() {
  let number = 54;
  printNumber();
}
// Prints 42
log();

上面代码,无论从何处调用 printNumber() 函数,console.log(number) 都将始终打印 42。这与具有动态作用域的语言不同,在「动态作用域」中,console.log(number) 将根据调用函数 printNumber() 的位置显示不同的值。

如果上面的代码是用支持「动态作用域」的语言编写的,那么 console.log(number) 会打印 54

使用「词法作用域」,我们可以仅通过查看源代码来确定变量的范围。而在动态范围界定的情况下,在执行代码之前无法确定范围。而在「动态作用域」界定的情况下,在执行代码之前无法确定范围。

像 C,C ++,Java,Javascript 等大多数编程语言都支持「静态作用域」。Perl 既支持「动态作用域」又支持静态作用域。

# 作用域链

⭐️⭐️⭐️ 当在JavaScript中使用变量时,JavaScript 引擎将尝试在「当前作用域」中查找变量的值。如果找不到变量,它将查找「外部作用域」并将继续这样做,直到找到变量或到达「全局作用域」为止。

如果仍然找不到该变量,它将在全局作用域内隐式声明该变量(如果不是在严格模式下)或返回错误。

例如:

let foo = 'foo';
function bar() {
  let baz = 'baz';
  // 打印 'baz'
  console.log(baz);
  // 打印 'foo'
  console.log(foo);
  number = 42;
  console.log(number);  // 打印 42
}
bar();

当执行函数 bar() 时,JavaScript 引擎将查找 baz 变量并在当前作用域中找到它。接下来,它会在当前作用域中查找 foo 变量,但无法在该作用域中找到它,因此它会在外部作用域中查找该变量并在其中找到它(即全局作用域)。

之后,我们将 42 分配给 number 变量,因此 JavaScript 引擎会在当前作用域内以及之后在外部作用域内查找 number 变量。

如果脚本不是在严格模式下,则引擎将创建一个名为 number 的新变量,并为其分配 42 或返回错误(如果是在严格模式下)。

因此,当使用变量时,引擎将遍历「作用域链」,直到找到该变量为止。

# 作用域和作用域链是如何工作的?

到目前为止,我们已经讨论了作用域是什么以及作用域的类型。现在,让我们了解 JavaScript 引擎如何确定变量的范围并在后台执行变量查找。

为了了解 JavaScript 引擎如何执行变量查找,我们必须了解 JavaScript 中的「词法环境」的概念。

# 什么是词法环境?

⭐️⭐️⭐️ 「词法环境」是保存标识符变量映射的结构。(此处,标识符是指变量/函数的名称,而变量是对实际对象(包括函数对象和数组对象)或原始值的引用)。

简而言之,词法环境是存储变量和对象引用的地方。

⭐️ 注意—不要将词法作用域与词法环境混淆,词法作用域是在编译时确定的作用域,而词法环境是在程序执行期间存储变量的地方。

从概念上讲,词法环境如下所示:

lexicalEnvironment = {
  a: 25,
  obj: <ref. to the object>
}

仅当执行该「词法作用域」中的代码时,引擎才为每个词法作用域创建一个新的「词法环境」。词法环境还引用了其外部词法环境(即外部作用域)。例如:

lexicalEnvironment = {
  a: 25,
  obj: <ref. to the object>
  outer: <outer lexical environemt>
}

# Javascript引擎是如何进行变量查找的?

现在我们知道了「作用域」,「作用域链」和「词法环境」,现在让我们了解 JavaScript 引擎如何使用词法环境来确定作用域和作用域链。

让我们看一下下面的代码片段,以了解以上概念。

let greeting = 'Hello';
function greet() {
  let name = 'Peter';
  console.log(`${greeting} ${name}`);
}
greet();
{
  let greeting = 'Hello World!'
  console.log(greeting);
}

加载上述脚本后,将创建一个「全局词法环境」,其中包含在「全局作用域」内定义的变量和函数。例如:

globalLexicalEnvironment = {
  greeting: 'Hello'
  greet: <ref. to greet function>
  outer: <null>
}

在这里,「外部词法环境」被设置为 null,因为在「全局作用域」之后没有「外部作用域」。

之后,会遇到对 greet() 函数的调用。因此,为 greet() 函数创建了一个新的词法环境。例如:

此处,「外部词法环境」被设置为 globalLexicalEnvironment,因为其外部作用域是「全局作用域」。

之后,JavaScript 引擎执行 console.log($ {greeting} $ {name}) 语句。

JavaScript 引擎尝试在函数的「词法环境」中查找 greetingname 变量。它可以在当前词法环境中找到 name 变量,但无法在当前词法环境中找到 greeting 变量。

因此,它在外部词法环境(由 outer 属性定义,即「全局词法环境」)中查找并找到 greeting 变量。

接下来,JavaScript 引擎在该块内的代码处执行。因此,它为该块创建了一个新的词法环境。例如:

blockLexicalEnvironment = {
  greeting: 'Hello World',
  outer: <globalLexicalEnvironment>
}

接下来,执行 console.log(greeting) 语句,JavaScript 引擎在当前词法环境中找到该变量并使用该变量。因此,它不会在变量的外部词法环境(全局词法环境)内部查找。

注意—仅为 letconst 声明而不是 var 声明创建一个新的词法环境。 var 声明被添加到当前词法环境(全局或函数词法环境)中。

⭐️⭐️⭐️ 因此,当在程序中使用变量时,JavaScript 引擎将尝试在当前词法环境中查找该变量,如果无法在该词法环境中找到该变量,它将在外部词法环境中查找该变量。这就是 JavaScript 引擎执行变量查找的方式

# 总结

简而言之,作用域是一个可见和可访问变量的区域。就像函数一样,JavaScript 中的作用域可以嵌套,JavaScript 引擎遍历作用域链以查找程序中使用的变量。

JavaScript 使用词法作用域,这意味着变量的作用域是在编译时确定的。 JavaScript 引擎使用词法环境在程序执行期间存储变量。

作用域和作用域链是每个 JavaScript 开发人员都应该理解的 JavaScript 基本概念。对这些概念有充分的了解将帮助您成为一个更有效,更好的 JavaScript 开发人员。

# 参考