阅读 360

前端面试必会 | 一文读懂 JavaScript 中的作用域和作用域链

本文翻译自 blog.bitsrc.io/understandi…,作者 Sukhjinder Arora,翻译时有部分删改,标题有修改。

作用域和作用域链是 JavaScript 和很多编程语言的基本概念。这些概念会让很多 JavaScript 开发者感到困惑,但是如果想掌握 JavaScript 它们又是必不可少的。

正确理解这些概念将有助于您编写更好,更有效和更干净的代码。反过来,它将帮助您成为更好的JavaScript开发人员。

因此,在本文中,我将解释什么是作用域和作用域链,以及 JavaScript 引擎如何进行变量查找和这些概念的内部原理。

什么是作用域

JavaScript 中的作用域是指变量的可访问性或可见性。也就是说,程序的哪些部分可以访问该变量,或者该变量在何处可见。

作用域为什么重要?

  1. 作用域的主要好处是安全性。也就是说,只能从程序的特定区域访问变量。使用作用域,我们可以避免程序其他部分对变量的意外修改。
  2. 作用域还减少了命名冲突。也就是说,我们可以在不同的范围内使用相同的变量名。

作用域类型

JavaScript 中有三种类型的作用域:

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

1. 全局作用域(Global Scope)

不在任何函数或块(一对花括号)内的任何变量都在全局作用域内。可以从程序的任何位置访问全局作用域内的变量。例如:

var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// Prints 'Hello World!'
greet();
复制代码

2. 局部作用域或者函数作用域

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

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// Prints 'Hello World!'
greet();
// Uncaught ReferenceError: greeting is not defined
console.log(greeting);
复制代码

3. 块级作用域

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

{
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// Prints 'English'
console.log(lang);
// Uncaught ReferenceError: greeting is not defined
console.log(greeting);
复制代码

作用域嵌套

就像 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();
复制代码

在这里,console.log(number) 总是会打印 42 无论 printNumber() 在何处被调用。这与动态作用域的语言不同,动态作用域语言中 printNumber() 在不同的位置执行将会打印不同的值。

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

使用词法作用域,我们可以仅通过查看源代码来确定变量的范围。而使用动态作用域,只有在执行代码后才能确定范围。

大多数编程语言都支持词法或静态作用域,例如 C,C++,Java,JavaScript。Perl 支持静态和动态作用域。

作用域链

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

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

例如:

let foo = 'foo';
function bar() {
  let baz = 'baz';
  // Prints 'baz'
  console.log(baz);
  // Prints 'foo'
  console.log(foo);
  number = 42;
  console.log(number);  // Prints 42
}
bar();
复制代码

执行 bar() 时,JavaScript 引擎将查找 baz 变量并在当前作用域中找到它。接下来,JavaScript 引擎会在当前作用域中查找 foo 变量,但无法在当前作用域中找到,所以引擎会在外层作用域中查找并找到这个变量。

之后我们给 number 变量赋值 42,JavaScript 引擎会先在当前作用域查找然后在外层作用域继续查找。

如果是在非严格模式下执行代码,引擎将会创建一个新变量 number,并给它赋值 42。如果运行在严格模式中将会报错。

严格模式下报错

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

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

到目前为止,我们已经讨论了什么是作用域和作用域的类型。接下来我们看看 JavaScript 引擎是如何定义变量的作用域的以及它是如何进行变量查找的。

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

词法环境是什么?

词法环境是用来保存标识符和变量映射关系的地方。标识符是变量或者函数的名字,变量是对实际对象(包括函数对象和数组对象)或者原始值的引用。

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

注意—不要把词法作用域词法环境混淆了。词法作用域是在编译时确定的作用域,而词法环境是在程序执行过程中存储变量的地方

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

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

当作用域内的代码执行的时候一个新的词法环境才会被创建。词法环境也有一个指向外部词法环境的引用 outer(外层作用域)。例如:

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() 创建一个新的词法环境。如下:

functionLexicalEnvironment = {
  name: 'Peter'
  outer: <globalLexicalEnvironment>
}
复制代码

这里把外部词法环境设置为 globalLexicalEnvironment,因为它的外部作用域是全局作用域。

之后,JavaScript 引擎将会执行 console.log(greeting + ' ' + name)

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

所以它在 greet 函数的外层词法环境(全局词法环境)中查找并找到了 greeting 变量。

接下来 JavaScript 引擎执行代码块内部的代码,引擎给代码块创建了一个新的词法环境。如下:

blockLexicalEnvironment = {
  greeting: 'Hello World',
  outer: <globalLexicalEnvironment>
}
复制代码

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

注意— JavaScript 引擎只会为 let const 声明的变量创建词法环境,不会为 var 声明的变量创建。 var 声明的变量会被添加到当前的词法环境(全局或者函数词法环境中)而不是块级词法环境中。

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

总结

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

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

作用域和作用域链是每个 JavaScript 开发人员都应理解的 JavaScript 基本概念。熟悉这些概念将帮助您成为一个更有效率、更优秀的 JavaScript 开发人员。

最后

往期精彩:

关注公众号可以看更多哦。

感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

公众号

关注下面的标签,发现更多相似文章
评论