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

本文翻译自 https://blog.bitsrc.io/understanding-scope-and-scope-chain-in-javascript-f6637978cf53,作者 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,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

公众号

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值