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

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

做用域和做用域链是 JavaScript 和不少编程语言的基本概念。这些概念会让不少 JavaScript 开发者感到困惑,可是若是想掌握 JavaScript 它们又是必不可少的。前端

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

所以,在本文中,我将解释什么是做用域和做用域链,以及 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,带你解读前端技术,掌握最本质的技能。关注公众号能够拉你进讨论群,有任何问题都会回复。

公众号
相关文章
相关标签/搜索