범위, 즉 변수가 위치해야 하는 위치를 결정하는 규칙 집합은 모든 프로그래밍 언어의 가장 기본적인 개념 중 하나입니다. 사실, 이는 너무 기본적이어서 이러한 규칙이 얼마나 미묘한지 우리는 쉽게 잊어버릴 수 있습니다!
JavaScript 엔진이 범위에 대해 어떻게 "생각"하는지 정확히 이해하면 호이스팅 작성으로 인해 발생할 수 있는 일반적인 실수를 방지하고 클로저에 집중할 수 있도록 준비하며 다시는 잘못된 것을 작성하지 않는 데 한 걸음 더 가까워질 수 있습니다.
...어쨌든 들어 올리고 닫는 것을 이해하는 데 도움이 될 것입니다.
이 글에서 우리는 다음에 대해 배울 것입니다:
let
和 const
게임을 바꾸는 방법더 자세히 살펴보겠습니다.
ES6에 대해 더 자세히 알아보고 구문과 기능을 활용하여 JavaScript 코드를 개선하고 단순화하는 방법에 관심이 있다면 다음 두 과정을 확인해 보세요.
이전에 JavaScript 코드를 작성해 본 적이 있다면 변수를 정의하는 위치가 변수를 사용하는 > 위치를 결정한다는 사실을 알게 될 것입니다. 변수의 가시성이 소스 코드의 구조에 따라 달라진다는 사실을 lexical scope라고 합니다.
JavaScript에서 범위를 만드는 방법에는 세 가지가 있습니다.
let
或 const
을 사용하세요. 이러한 선언은 블록 내에서만 표시됩니다.
catch
블록. 믿거나 말거나, 이것은 실제로 정말새로운 범위를 만들어냅니다!
와 잘 작동하지 않습니다. let
편집 과정: 조감도
컴파일단계 동안 JavaScript 엔진은 다음을 수행합니다.
실행 중에만 JavaScript 엔진이 실제로 변수 참조 값을 할당된 값과 동일하게 설정합니다. 그때까지는 입니다. undefined
먼저
var first_name = "Peleke"
줄을 읽습니다. 다음으로 변수를 저장할 scopevar first_name = "Peleke"
。接下来,它确定将变量保存到的范围。因为我们位于脚本的顶层,所以它意识到我们处于全局范围。然后,它将变量 first_name
保存到全局范围,并将其值初始化为 undefined
를 결정합니다. 우리는 스크립트의 최상위 수준에 있기 때문에 우리가
에 있음을 인식합니다. 그런 다음 first_name
변수를 전역 범위에 저장하고 해당 값을 function popup (first_name)
로 초기화합니다.
두 번째로 컴파일러는
functionvar last_name = "Sengstacke"
,因此编译器会将变量 last_name
保存到 class="inline">popup
—不到全局范围 — 并将其值设置为 undefined
키워드는 줄의 첫 번째 항목이므로 함수에 대한 새 범위를 만들고 함수 정의를 전역 범위에 등록한 다음 내부를 살펴보고 변수 선언을 찾습니다.
물론, 컴파일러가 하나를 찾았습니다. 함수의 첫 번째 줄에 var last_name = "Sengstacke"
가 있으므로 컴파일러는 last_name
변수를
class="inline">popup — 전역 범위가 아니라 — 값을
로 설정합니다.함수 내에 더 이상 변수 선언이 없으므로 컴파일러는 전역 범위로 대체됩니다. 더 이상 변수 선언이 없으므로 이 단계는 완료됩니다. 🎜 🎜아직 실제로 아무것도 실행하지 않았음을 참고하세요. 🎜🎜이 시점에서 컴파일러의 임무는 모든 사람의 이름을 알고 있는지 확인하는 것입니다. 🎜 🎜이 시점에서 우리 프로그램은 다음을 알고 있습니다. 🎜
first_name
的变量。 的函数。
范围内有一个名为 last_name
的变量。
first_name
和 last_name
的值均为 undefined
。它并不关心我们是否在代码中的其他地方分配了这些变量值。引擎在执行时会处理这个问题。
在下一步中,引擎再次读取我们的代码,但这一次,执行它。
首先,它读取行 var first_name = "Peleke"
。为此,引擎会查找名为 first_name
的变量。由于编译器已经用该名称注册了一个变量,引擎会找到它,并将其值设置为 "Peleke"
。
接下来,它读取行 function popup (first_name)
。由于我们没有在这里执行该函数,因此引擎不感兴趣并跳过它。
最后,它读取行 。由于我们在这里执行一个函数,因此引擎:
的值
first_name
的值
作为函数执行,并传递 first_name
的值作为参数
当执行 时,它会经历相同的过程,但这次是在函数
内。它:
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
。已损坏的
,但您会收到 TypeError
。bar
,它会按需要工作。提升是指 JavaScript 使我们声明的所有变量名称在其作用域内的任何地方都可用,包括在我们分配给它们之前 .
代码段中的三种情况是您在自己的代码中需要注意的三种情况,因此我们将一一逐步介绍它们。
请记住,当 JavaScript 编译器读取像 var foo = "bar"
这样的行时,它会:
foo
注册到最近的范围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读取时,它更有意义,您不觉得吗?
查看:
undefined
。现在让我们来看看两个工作方式稍有不同的新工具:let
和 const
。
<strong>let</strong>
、<strong></strong>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
相同,但有两个主要区别:
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
或 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
声明变量,则该变量仅在块内可见,且仅< /em> 分配后。
但是,使用 var
声明的变量在尽可能远的地方可见 - 在本例中是在全局范围内。
如果您对 let
和 const
的具体细节感兴趣,请查看 Rauschmayer 博士在《探索 ES6:变量和范围》中对它们的介绍,并查看有关它们的 MDN 文档。
<strong>this</strong>
和箭头函数从表面上看,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를 배우는 데 도움이 되는 완벽한 가이드를 만들었습니다.
위 내용은 JavaScript의 범위 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!