准确了解javascript引擎如何“思考”作用域将使您避免编写提升可能导致的常见错误,让您准备好围绕闭包进行思考,并让您更接近永远不再编写 错误。
...好吧,无论如何,它会帮助您了解提升和关闭。
在本文中,我们将看看:
JavaScript 范围的基础知识
解释器如何决定哪些变量属于什么范围
吊装的 真正工作原理
词法作用域
如果你以前写过一行 javaScript,你就会知道 定义变量的位置决定了可以 使用它们的位置。变量的可见性取决于源代码的结构这一事实称为词法 范围。
在 JavaScript 中创建作用域的三种方法:
创建一个函数. 在函数内部声明的变量仅在该函数内部可见,包括在嵌套函数中。
let用代码块或 const 在代码块内声明变量 。此类声明仅在块内可见。
创建一个 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 时,会发生两件事让它工作。
首先,您的源代码被编译。
然后,编译的代码被执行。
期间 编译步骤,JavaScript 引擎:
记下所有变量名
在适当的范围内注册它们
为自己的价值观保留空间
只有在 执行期间,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到 范围popup——不是全局范围——并将其值设置为 undefined.
由于函数内部不再有变量声明,编译器会退回到全局范围。并且由于那里没有更多的变量声明 ,这个阶段就完成了。
请注意,我们实际上还没有 运行任何东西。编译器此时的工作只是确保它知道每个人的名字。它不关心他们做什么。
此时,我们的程序知道:
first_name在全局范围内调用了一个变量 。
popup在全局范围内调用了一个函数。
last_name在 的范围内 调用了一个变量popup。
first_name和 的值 last_name 都是 undefined。
它并不关心我们是否在代码的其他地方分配了这些变量值。引擎在执行时会处理这些问题。
第 2 步:执行
在下一步中,引擎再次读取我们的代码,但这一次 执行它。
首先,它读取行, var first_name = "Peleke". 为此,引擎会查找名为 的变量 first_name。由于编译器已经使用该名称注册了一个变量,因此引擎会找到它,并将其值设置为"Peleke".
接下来,它读取行, function popup (first_name). 由于我们没有在此处执行该函数,因此引擎不感兴趣并跳过它。
最后,它读取行 popup(first_name)。由于我们 在这里执行一个函数,引擎:
查找的值 popup
查找的值 first_name
popup作为函数 执行,将 的值first_name作为参数传递
当它执行时popup,它会经历同样的过程,但这次是在函数内部 popup。它:
查找名为的变量 last_name
设置 last_name的值等于"Sengstacke"
查找 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!"); }
如果你运行这段代码,你会注意到三件事:
您可以foo 在分配之前 参考 它,但它的值是undefined.
您 可以在定义之前 调用 broken 它,但您会得到一个 TypeError.
您可以在定义它之前调用 bar它,它可以按需要工作。
提升是指 JavaScript 使我们声明的所有变量名在其范围内的任何地方都可用 - 包括 在我们分配给它们之前。
代码段中的三种情况是您在自己的代码中需要注意的三种情况,因此我们将逐一介绍它们。
提升变量声明
请记住,当 JavaScript 编译器读取类似 的行时 var foo = "bar",它:
将名称注册 foo到最近的范围
将 的值设置 foo为未定义
我们可以foo在分配之前使用它的原因是,当引擎查找具有该名称的变量时,它确实存在。这就是为什么它不抛出 ReferenceError.
相反,它会获取 value 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 的话说,这有助于您的代码 阅读起来更像是 运行。
当您考虑它时,这是有道理的。很清楚为什么bar当我们以 JavaScript 读取代码的方式编写代码时的行为方式,不是吗?那么为什么不一直这样写呢?
提升函数表达式
TypeError我们在定义之前尝试执行时 得到了一个事实,broken这只是提升第一规则的一个特例。
我们定义了一个变量,称为 broken,编译器在全局范围内注册并设置为 undefined。当我们尝试运行它时,引擎会查找 的值 broken,发现它是 undefined,并尝试 undefined作为函数执行。
显然,undefined is 不是一个函数——这就是为什么我们得到一个 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读取那样编写 时,它更有意义,您不觉得吗?
回顾:
变量声明和函数表达式的名称在它们的范围内都是可用的,但它们的 值 直到undefined赋值。
函数声明的名称 和定义在其范围内都可用,甚至在 定义之前。
现在让我们来看看两种工作方式略有不同的新工具: let和 const.
let, const, & 时间死区
与 var 声明不同,使用声明的变量 let 不会 const 被 编译器提升。
至少,不完全是。
还记得我们如何能够跟注,但却因为我们试图执行 broken而得到一个 吗?如果我们用 定义,我们会得到一个 ,而不是:TypeErrorundefinedbrokenletReferenceError
"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。相反,它使变量未初始化, 直到 引擎执行您的赋值语句。访问未初始化变量的值会抛出 a ReferenceError,这解释了为什么上面的代码片段在我们运行时会抛出。
声明范围顶部 的开头let 和赋值语句 之间的空间称为时间死区。这个名字来源于这样一个事实,即使引擎知道foo在 的范围顶部 调用 bar的变量,该变量是“死的”,因为它没有值。
...也因为如果您尝试尽早使用它,它会杀死您的程序。
关键字的 const工作方式与 相同 let,但有两个主要区别:
声明时必须 赋值 const。
您 不能将值重新分配给用 . 声明的变量 const。
这保证了 const它将 始终 具有您最初分配给它的值。
// This is legal const react = require('react'); // This is totally not legal const crypto; crypto = require('crypto');
块范围
let并且与 另一种方式const不同 var:它们的范围大小。
当你用 声明一个变量时 var,它是可见的 在作用域链上尽可能高——通常在最近的函数声明的顶部,或者在全局作用域中,如果你在顶层声明它。
let但是,当您使用or 声明变量时 ,它尽可能在本地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如果您对and 的细节感兴趣const,请查看 Rauschmayer 博士在Exploring ES6: Variables and Scoping中对它们的看法,并查看有关它们的 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 lexically的含义。
但事实并非如此。
相反,它会在您定义的每个函数中创建一个新 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 Fundamentals优秀课程 ,以及有关箭头函数的 MDN 文档。
结论
到目前为止,我们已经覆盖了很多领域!在本文中,您了解到:
变量在 编译期间注册到它们的作用域,并在执行期间与它们的赋值相关联 。
引用在赋值时或 之前声明的变量会抛出 a ,并且这些变量的作用域是最近的块。 letconstReferenceError
箭头函数 允许我们实现 的词法绑定 this,并绕过传统的动态绑定。
您还看到了提升的两条规则:
提升的 第一条规则:函数表达式和 var 声明在定义它们的范围内都可用,但在 undefined 赋值语句执行之前具有值。
提升的 第二条规则:函数声明的名称 及其 主体在定义它们的范围内都是可用的。
- 第 1 步:编译
- 第 2 步:执行
- 提升变量声明
- 提升函数表达式
- 提升函数声明
- let, const, 时间死区
- 块范围
- 词汇 this和箭头函数