logo头像

云影sky

前端面试必会 | 一文读懂 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(<span class="hljs-subst" style="color: #a6e22e; line-height: 26px;">${lang}</span>: <span class="hljs-subst" style="color: #a6e22e; line-height: 26px;">${greeting}</span> <span class="hljs-subst" style="color: #a6e22e; line-height: 26px;">${name}</span>);
}
}
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,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。


公众号
公众号