本文翻译自 https://blog.bitsrc.io/understanding-scope-and-scope-chain-in-javascript-f6637978cf53,作者 Sukhjinder Arora,翻译时有部分删改,标题有修改。
作用域和作用域链是 JavaScript 和很多编程语言的基本概念。这些概念会让很多 JavaScript 开发者感到困惑,但是如果想掌握 JavaScript 它们又是必不可少的。
正确理解这些概念将有助于您编写更好,更有效和更干净的代码。反过来,它将帮助您成为更好的JavaScript开发人员。
因此,在本文中,我将解释什么是作用域和作用域链,以及 JavaScript 引擎如何进行变量查找和这些概念的内部原理。
什么是作用域
JavaScript 中的作用域是指变量的可访问性或可见性。也就是说,程序的哪些部分可以访问该变量,或者该变量在何处可见。
作用域为什么重要?
作用域的主要好处是安全性。也就是说,只能从程序的特定区域访问变量。使用作用域,我们可以避免程序其他部分对变量的意外修改。
作用域还减少了命名冲突。也就是说,我们可以在不同的范围内使用相同的变量名。
作用域类型
JavaScript 中有三种类型的作用域:
全局作用域;
函数作用域;
块作用域;
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 引入了 let
和 const
变量,与 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
。如果运行在严格模式中将会报错。
![](https://i-blog.csdnimg.cn/blog_migrate/6964f6fed40a41ecb1ad2718e1bdecc1.png)
因此,当使用变量时,引擎将遍历作用域链,直到找到该变量为止。
作用域和作用域链是如何工作的?
到目前为止,我们已经讨论了什么是作用域和作用域的类型。接下来我们看看 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 引擎尝试在函数的词法环境中查找 greeting
和 name
变量,它可以在当前词法环境中找到 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,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。
![](https://i-blog.csdnimg.cn/blog_migrate/356c7f8f3b2b2cc09f0845137e62a1ac.png)