### [理解JavaScript中的作用域](https://www.prosaas.cn/article/6393) **Published:** 2023-08-31T12:17:06 **Author:** 5201314 **Excerpt:** 范围,或确定变量所在位置的一组规则,是任何编程语言的最基本概念之一。事实上,它是如此基本,以至于我们很容易忘记… ![](//zhiqipro.oss-cn-beijing.aliyuncs.com/2023/12/20231212125006614.jpg) 范围,或确定变量所在位置的一组规则,是任何编程语言的最基本概念之一。事实上,它是如此基本,以至于我们很容易忘记这些规则是多么微妙! 准确理解 JavaScript 引擎如何“思考”作用域将使您避免编写提升可能导致的常见错误,让您做好准备专注于闭包,并让您离永远不再编写错误更近一步 再次。 ...无论如何,它会帮助您理解提升和关闭。 在本文中,我们将了解: - JavaScript 中作用域的基础知识 - 解释器如何决定哪些变量属于哪个作用域 - 吊装的实际工作原理 - ES6 关键字 `let` 和 `const` 如何改变游戏规则 让我们深入探讨。 如果您有兴趣了解有关 ES6 的更多信息以及如何利用语法和功能来改进和简化 JavaScript 代码,为什么不看看这两门课程: ## 词法范围 如果您之前编写过一行 JavaScript,您就会知道定义变量的位置决定了您可以_使用_的位置> 他们。变量的可见性取决于源代码的结构,这一事实称为_词法_ _范围。_ 在 JavaScript 中创建作用域有三种方法: 1. **创建函数**。在函数内部声明的变量仅在该函数内部可见,包括在嵌套函数中。 2. **使用 `let` 或 `const` **在代码块内**声明变量。此类声明仅在块内可见。 ** 3. **创建**`catch` **块**。不管你相信与否,这实际上_确实_创建了一个新的范围! "use strict"; var mr\_global = "Mr Global"; function foo () { var mrs\_local = "Mrs Local"; console.log("I can see " + mr\_global + " and " + mrs\_local + "."); function bar () { console.log("I can also see " + mr\_global + " and " + mrs\_local + "."); } } foo(); // Works as expected try { console.log("But /I/ can't see " + mrs\_local + "."); } catch (err) { console.log("You just got a " + err + "."); } { let foo = "foo"; const bar = "bar"; console.log("I can use " + foo + bar + " in its block..."); } try { console.log("But not outside of it."); } catch (err) { console.log("You just got another " + err + "."); } // Throws ReferenceError! console.log("Note that " + err + " doesn't exist outside of 'catch'!") 上面的代码片段演示了所有三种作用域机制。您可以在 Node 或 Firefox 中运行它,但 Chrome 还无法与 `let` 很好地配合。 我们将详细讨论其中的每一个。让我们首先详细了解 JavaScript 如何确定哪些变量属于哪个作用域。 ## 编译过程:鸟瞰 当您运行一段 JavaScript 时,会发生两件事使其正常工作。 1. 首先,编译您的源代码。 2. 然后,编译后的代码将被执行。 在_编译__步骤_期间,JavaScript 引擎: 1. 记下所有变量名称 2. 将它们注册到适当的范围 3. 为自己的价值观保留空间 只有在_执行_期间,JavaScript 引擎才真正将变量引用的值设置为等于其赋值。在那之前,它们是 `undefined`。 ### 第 1 步:编译 // I can use first\_name anywhere in this program var first\_name = "Peleke"; function popup (first\_name) { // I can only use last\_name inside of this function var last\_name = "Sengstacke"; alert(first\_name + ' ' + last\_name); } popup(first\_name); 让我们逐步了解一下编译器的作用。 首先,它读取行 `var first_name = "Peleke"`。接下来,它确定将变量保存到的_范围_。因为我们位于脚本的顶层,所以它意识到我们处于_全局范围_。然后,它将变量 `first_name` 保存到全局范围,并将其值初始化为 `undefined`。 其次,编译器读取带有 `function popup (first_name)` 的行。因为 _function_ 关键字是该行的第一个内容,因此它为函数创建一个新作用域,将函数的定义注册到全局作用域,并查看内部以查找变量声明。 果然,编译器找到了一个。由于我们函数的第一行中有 `var last_name = "Sengstacke"`,因此编译器会将变量 `last_name` 保存到 class="inline">popup —_不_到全局范围 — 并将其值设置为 `undefined`。 由于函数内不再有变量声明,编译器将退回到全局作用域。由于不再有变量声明,因此此阶段已完成。 请注意,我们实际上还没有运行任何东西。此时编译器的工作只是确保它知道每个人的名字;它不关心他们做什么。 此时,我们的程序知道: 1. 全局范围内有一个名为 `first_name` 的变量。 2. 全局范围内有一个名为 `popup` 的函数。 3. 在 `popup` 范围内有一个名为 `last_name` 的变量。 4. `first_name` 和 `last_name` 的值均为 `undefined`。 它并不关心我们是否在代码中的其他地方分配了这些变量值。引擎在执行时会处理这个问题。 ### 第 2 步:执行 在下一步中,引擎再次读取我们的代码,但这一次,_执行_它。 首先,它读取行 `var first_name = "Peleke"`。为此,引擎会查找名为 `first_name` 的变量。由于编译器已经用该名称注册了一个变量,引擎会找到它,并将其值设置为 `"Peleke"`。 接下来,它读取行 `function popup (first_name)`。由于我们没有在这里执行该函数,因此引擎不感兴趣并跳过它。 最后,它读取行 `popup(first_name)`。由于我们在这里执行一个函数,因此引擎: 1. 查找 `popup` 的值 2. 查找 `first_name` 的值 3. 将 `popup` 作为函数执行,并传递 `first_name` 的值作为参数 当执行 `popup` 时,它会经历相同的过程,但这次是在函数 `popup` 内。它: 1. 查找名为 `last_name` 的变量 2. 将 `last_name` 的值设置为等于 `"Sengstacke"` 3. 查找 `alert`,将其作为函数执行,并以 `"Peleke Sengstacke"` 作为参数 事实证明,幕后发生的事情比我们想象的要多得多! 既然您已经了解了 JavaScript 如何读取和运行您编写的代码,我们就准备好解决一些更贴近实际的问题:提升的工作原理。 ## 显微镜下的吊装 让我们从一些代码开始。 bar(); function bar () { if (!foo) { alert(foo + "? This is strange..."); } var foo = "bar"; } broken(); // TypeError! var broken = function () { alert("This alert won't show up!"); } 如果运行此代码,您会注意到三件事: 1. 在分配之前,您_可以_引用 `foo`,但其值为 `undefined`。 2. 您可以在定义之前调用 `已损坏的`,但您会收到 `TypeError`。 3. 您_可以_在定义之前调用 `bar`,它会按需要工作。 **提升**是指 JavaScript 使我们声明的所有变量名称在其作用域内的_任何地方_都可用,包括在我们分配给它们之前 . 代码段中的三种情况是您在自己的代码中需要注意的三种情况,因此我们将一一逐步介绍它们。 ### 提升变量声明 请记住,当 JavaScript 编译器读取像 `var foo = "bar"` 这样的行时,它会: 1. 将名称 `foo` 注册到最近的范围 2. 将 `foo` 的值设置为未定义 我们可以在赋值之前使用 `foo` 的原因是,当引擎查找具有该名称的变量时,它_确实_存在。这就是为什么它不会抛出 `ReferenceError`。 相反,它获取值 `undefined`,并尝试使用该值执行您要求的任何操作。通常,这是一个错误。 记住这一点,我们可能会想象 JavaScript 在我们的函数 `bar` 中看到的更像是这样: function bar () { var foo; // undefined if (!foo) { // !undefined is true, so alert alert(foo + "? This is strange..."); } foo = "bar"; } 如果您愿意的话,这是**提升的第一条规则**:变量_在其整个范围内_都可用,但其值为 `undefined` ,直到您代码分配给他们。 常见的 JavaScript 习惯用法是将所有 `var` 声明写入其作用域的顶部,而不是首次使用它们的位置。用 Doug Crockford 的话来说,这可以帮助您的代码_阅读_更像它_运行_。 仔细想想,这是有道理的。当我们以 JavaScript 读取代码的方式编写代码时,为什么 `bar` 的行为方式非常清楚,不是吗?那么为什么不一直这样写呢? ### 提升函数表达式 事实上,当我们在定义之前尝试执行 `broken` 时,我们得到了 `TypeError`,这只是第一条提升规则的一个特例。 我们定义了一个名为 `broken` 的变量,编译器会在全局范围内注册该变量,并将其设置为等于 `undefined`。当我们尝试运行它时,引擎会查找 `broken` 的值,发现它是 `undefined`,并尝试将 `undefined` 作为函数执行. 显然,`undefined` _不是_一个函数,这就是为什么我们得到 `TypeError`! ### 提升函数声明 最后,回想一下,在定义 `bar` 之前,我们可以调用它。这是由于**第二条提升规则**:当 JavaScript 编译器找到函数声明时,它会使其名称和定义在其作用域的顶部可用。再次重写我们的代码: function bar () { if (!foo) { alert(foo + "? This is strange..."); } var foo = "bar"; } var broken; // undefined bar(); // bar is already defined, executes fine broken(); // Can't execute undefined! broken = function () { alert("This alert won't show up!"); } 同样,当您_编写_作为JavaScript_读取_时,它更有意义,您不觉得吗? 查看: 1. 变量声明和函数表达式的名称在其整个范围内都可用,但它们的值在赋值之前为 `undefined`。 2. 函数声明的名称_​​和_定义在其整个范围内都可用,_甚至在_其定义之前。 现在让我们来看看两个工作方式稍有不同的新工具:`let` 和 `const`。 ### `**let**`**、**`const` **和临时死区** 与 `var` 声明不同,使用 `let` 和 `const` 声明的变量_不_ 被编译器提升。 至少,不完全是。 还记得我们如何调用 `已损坏的`,但却因为尝试执行 `undefined` 而收到 `TypeError` 吗?如果我们使用 `let` 定义 `broken`,我们就会得到 `ReferenceError`,而不是: "use strict"; // You have to "use strict" to try this in Node broken(); // ReferenceError! let broken = function () { alert("This alert won't show up!"); } 当 JavaScript 编译器在第一遍中将变量注册到其作用域时,它对待 `let` 和 `const` 的方式与处理 `var` 的方式不同。 当它找到 `var` 声明时,我们将该变量的名称注册到其范围,并立即将其值初始化为 `undefined`。 但是,使用 `let`,编译器_会_将变量注册到其作用域,但_不会_初始化其值为 `undefined`。相反,它会使变量保持未初始化状态,直到引擎执行您的赋值语句。访问未初始化变量的值会抛出 `ReferenceError`,这解释了为什么上面的代码片段在运行时会抛出异常。 `let` 声明和赋值语句的顶部开头之间的空间称为临时死区。该名称源自以下事实:即使引擎_知道_名为 `foo` 的变量(位于 `bar` 范围的顶部),该变量是“死的”,因为它没有值。 ...还因为如果您尝试尽早使用它,它会杀死您的程序。 `const` 关键字的工作方式与 `let` 相同,但有两个主要区别: 1. 使用 `const` 声明时,_必须_分配一个值。 2. 您_不能_为使用 `const` 声明的变量重新赋值。 这可以保证 `const` 将_始终_拥有您最初分配给它的值。 // This is legal const React = require('react'); // This is totally not legal const crypto; crypto = require('crypto'); ### **块范围** `let` 和 `const` 与 `var` 在另一方面有所不同:它们的范围大小。 当您使用 `var` 声明变量时,它在作用域链的尽可能高的位置可见 - 通常是在最近的函数声明的顶部,或者在全局范围,如果您在顶层声明它。 但是,当您使用 `let` 或 `const` 声明变量时,它会尽可能_本地_可见 -_仅_ > 在最近的街区内。 块是由大括号分隔的一段代码,如 `if`/`else` 块、`for` 循环,以及显式“阻止”的代码块,如本段代码所示。 "use strict"; { let foo = "foo"; if (foo) { const bar = "bar"; var foobar = foo + bar; console.log("I can see " + bar + " in this bloc."); } try { console.log("I can see " + foo + " in this block, but not " + bar + "."); } catch (err) { console.log("You got a " + err + "."); } } try { console.log( foo + bar ); // Throws because of 'foo', but both are undefined } catch (err) { console.log( "You just got a " + err + "."); } console.log( foobar ); // Works fine 如果您在块内使用 `const` 或 `let` 声明变量,则该变量_仅_在块内可见,且_仅 分配后。_ 但是,使用 `var` 声明的变量在尽可能远的地方可见 - 在本例中是在全局范围内。 如果您对 `let` 和 `const` 的具体细节感兴趣,请查看 Rauschmayer 博士在《探索 ES6:变量和范围》中对它们的介绍,并查看有关它们的 MDN 文档。 ### 词法 `**this**` 和箭头函数 从表面上看,`this` 似乎与范围没有太大关系。事实上,JavaScript 并没有根据我们在这里讨论的范围规则来解析 `this` 的含义。 至少,通常不会。众所周知,JavaScript 不会根据您使用该关键字的位置来解析 `this` 关键字的含义: var foo = { name: 'Foo', languages: \['Spanish', 'French', 'Italian'\], speak : function speak () { this.languages.forEach(function(language) { console.log(this.name + " speaks " + language + "."); }) } }; foo.speak(); 我们大多数人都认为 `this` 表示 `foo` 在 `forEach` 循环内,因为这就是它在循环之外的含义。换句话说,我们期望 JavaScript 能够解析 `this` _词法_的含义。 但事实并非如此。 相反,它会在您定义的每个函数中创建一个_新_ `this`,并根据您_如何_调用该函数来决定其含义 -不是您定义它的_位置_。 第一点类似于在子作用域中重新定义_任何_变量的情况: function foo () { var bar = "bar"; function baz () { // Reusing variable names like this is called "shadowing" var bar = "BAR"; console.log(bar); // BAR } baz(); } foo(); // BAR 将 `bar` 替换为 `this`,整个事情应该立即清楚! 传统上,要让 `this` 按照我们期望的普通旧词法范围变量的方式工作,需要以下两种解决方法之一: var foo = { name: 'Foo', languages: \['Spanish', 'French', 'Italian'\], speak\_self : function speak\_s () { var self = this; self.languages.forEach(function(language) { console.log(self.name + " speaks " + language + "."); }) }, speak\_bound : function speak\_b () { this.languages.forEach(function(language) { console.log(this.name + " speaks " + language + "."); }.bind(foo)); // More commonly:.bind(this); } }; 在 `speak_self` 中,我们将 `this` 的含义保存到变量 `self` 中,并使用该变量来得到我们想要的参考。在 `speak_bound` 中,我们使用 `bind` 来_永久_将 `this` 指向给定对象。 ES2015 为我们带来了一种新的选择:箭头函数。 与“普通”函数不同,箭头函数_不会_通过设置自己的值来隐藏其父作用域的 `this` 值。相反,他们从词汇上解析其含义。 换句话说,如果您在箭头函数中使用 `this`,JavaScript 会像查找任何其他变量一样查找其值。 首先,它检查本地范围内的 `this` 值。由于箭头函数没有设置一个,因此它不会找到一个。接下来,它检查 `this` 值的_父_范围。如果找到,它将使用它。 这让我们可以像这样重写上面的代码: var foo = { name: 'Foo', languages: \['Spanish', 'French', 'Italian'\], speak : function speak () { this.languages.forEach((language) => { console.log(this.name + " speaks " + language + "."); }) } };    如果您想了解有关箭头函数的更多详细信息,请查看 Envato Tuts+ 讲师 Dan Wellman 的有关 JavaScript ES6 基础知识的精彩课程,以及有关箭头函数的 MDN 文档。 ## 结论 到目前为止,我们已经了解了很多内容!在本文中,您了解到: - 变量在编译期间注册到其作用域,并在执行期间与其赋值相关联。 - 在赋值之前引用使用 `let` 或 class="inline">const 声明的变量会引发 `ReferenceError`,并且此类变量是范围到最近的块。 - 箭头函数 允许我们实现 `this` 的词法绑定,并绕过传统的动态绑定。 您还了解了提升的两条规则: - **第一条提升规则**:函数表达式和 `var` 声明在其定义的整个范围内都可用,但其值为 `undefined` 直到您的赋值语句执行。 - **第二条提升规则**:函数声明的名称及其主体在定义它们的范围内可用。 下一步最好是利用 JavaScript 作用域的新知识来理解闭包。为此,请查看 Kyle Simpson 的 Scopes & Closures。 最后,关于 `this` 有很多话要说,我无法在此介绍。如果该关键字看起来仍然像是黑魔法,请查看此和对象原型来了解它。 与此同时,利用您所学到的知识并减少编写错误! **学习 JavaScript:完整指南** 我们构建了一个完整的指南来帮助您学习 JavaScript,无论您是刚刚开始作为 Web 开发人员还是想探索更高级的主题。 以上就是理解JavaScript中的作用域的详细内容,更多请关注知企PROSAAS其它相关文章! **Tags:** alert, catch, chrome, class, const, es6, firefox, for, foreach, function **Categories:** WordPress ---