JavaScript 中的作用域链
第三章:作用域链
第三章:作用域链
- 这是《你不知道的 JS:作用域和闭包》这本书第三章的笔记。
- 嵌套在其他作用域中的作用域之间的连接称为作用域链。
- 作用域链是有向的,即查找只能向上移动。
“查找”主要是一个概念
- 在上一章中,我们将运行时对变量的访问描述为查找,其中 JavaScript 引擎首先检查变量是否存在于当前作用域中,然后沿着嵌套作用域链向上移动(朝向全局作用域),直到找到该变量(如果有的话)。
- 一旦找到范围内第一个匹配的命名声明,查找就会停止。
- 变量的作用域通常在初始编译过程中确定。它不会因为运行时可能发生的任何事情而改变。
- 由于范围是从编译时已知的,因此该信息很可能与 AST 中每个变量的条目一起存储,这意味着引擎不需要查找一堆范围来确定变量来自哪个范围。
- 避免查找的需要是词法范围的一个关键优化优势。
注意:考虑以下场景:我们有多个文件,但无法在其中一个文件中找到特定变量的声明。如果找不到声明,这并不总是错误。该变量可能是由另一个文件(程序)在运行时在共享全局作用域中声明的。
- 因此,对于变量是否在某个范围内声明的最终确定可能需要推迟到运行时。
- 让我们通过上一章讨论过的大理石和水桶的类比来理解这一点:
任何对最初未声明的变量的引用在该文件编译期间都会保留为未着色的弹珠;直到其他相关文件编译完成并应用程序运行时开始后,才能确定其颜色。该延迟查找最终会将颜色解析到该变量所在的作用域(可能是全局作用域)。
阴影
- 如果所有变量都有不同的名称,那么即使所有变量都在全局范围内声明也无所谓。
- 当你有两个或多个变量,每个变量位于不同的作用域,但具有相同的词汇名称时,拥有不同的词汇作用域就开始变得更加重要。
- 让我们考虑一个例子:
var studentName = "Suzy";
function printStudent(studentName) {
studentName = studentName.toUpperCase();
console.log(studentName);
}
printStudent("Frank");
// FRANK
printStudent(studentName);
// SUZY
console.log(studentName);
// Suzy
- 第 1 行的声明
studentName
在全局范围内创建了一个新变量。 studentName
函数中的三个引用printStudent
指向不同的局部作用域变量,而不是全局作用域studentName
变量。这种行为被称为“阴影”。- 因此,我们可以说在上面的例子中,局部作用域变量遮蔽了全局作用域变量。
注意:从词法上讲,在 printStudent(..) 函数内部的任何地方(或任何嵌套范围)引用全局 studentName 是不可能的。
全局解除阴影技巧
- 可以从变量被遮蔽的范围访问全局变量,但不能通过典型的词汇标识符引用。
- 在全局作用域中,
var
声明function
也会将自身暴露为全局对象的属性(与标识符同名),全局对象本质上是全局作用域的对象表示。考虑以下程序:
var studentName = "Suzy";
function printStudent(studentName) {
console.log(studentName);
console.log(window.studentName);
}
printStudent("Frank");
// "Frank"
// "Suzy"
- 因此,正如我们所注意到的,
window.variableName
我们仍然可以在函数中访问全局范围的阴影变量。
笔记:
- 这
window.studentName
是全局变量的镜像studentName
,而不是单独的快照副本。无论朝哪个方向,对其中一个变量的更改都会在另一个变量中可见。 - 此技巧仅适用于访问全局范围变量,而不适用于访问嵌套范围中的阴影变量,即使如此,也只适用于使用
var
或声明的变量function
。
警告:可以不代表应该这么做。不要隐藏你需要访问的全局变量;反之,也不要用这个技巧来访问已经被隐藏的全局变量。
复制不等于访问
- 考虑以下示例:
var special = 42;
function lookingFor(special) {
var another = {
special: special,
};
function keepLooking() {
var special = 3.141592;
console.log(special);
console.log(another.special); // Ooo, tricky!
console.log(window.special);
}
keepLooking();
}
lookingFor(112358132134);
// 3.141592
// 112358132134
// 42
- 所以,我们注意到我们能够在函数中获取
special
作为参数传递给函数的变量的值。这是否意味着我们访问了一个隐藏变量?lookingFor
keepLooking
- 不!
special: special
这是将参数变量的值复制special
到另一个容器(同名属性)中。这并不意味着我们正在访问参数special
。而是我们通过另一个容器访问它当时的值的副本。我们不能special
在函数内部将参数重新赋值给另一个值keepLooking
。 - 如果我使用对象或数组作为值而不是数字(例如 112358132134 等)会怎么样?使用对象的引用而不是原始值的副本能“修复”这种不可访问性吗?不能。通过引用副本修改对象值的内容与通过词法访问变量本身不同。我们仍然无法重新赋值
special
参数。
非法跟踪
- 并非所有声明隐藏的组合都是允许的。例如,
let
can shadowvar
,但var
can't shadowlet
。考虑以下示例:
function something() {
var special = "JavaScript";
{
let special = 42; // totally fine shadowing
// ..
}
}
function another() {
// ..
{
let special = "JavaScript";
{
var special = 42;
// ^^^ Syntax Error
// ..
}
}
}
- 请注意
another()
,在函数中,内部 varspecial
声明试图声明一个函数范围的special
,这本身是没问题的(如something()
函数所示)。 - 在这种情况下,语法错误描述表明
special
已经定义。 - 它被提出为 的真正原因
SyntaxError
是因为var
基本上试图“跨越”(或跳过)let
同名声明的边界,这是不允许的。 - 跨边界的禁止实际上在每个函数边界处停止,因此这个变体不会引发任何异常:
function another() {
// ..
{
let special = "JavaScript";
ajax("https://some.url", function callback() {
// totally fine shadowing
var special = "JavaScript";
// ..
});
}
}
函数名称作用域
- 函数声明如下所示:
function askQuestion() {
// ..
}
- 而函数表达式如下所示:
var askQuestion = function(){
//..
};
- 函数表达式将函数作为值,因此函数本身不会“提升”。
- 现在让我们考虑一个命名函数表达式:
var askQuestion = function ofTheTeacher() {
// ..
};
- 我们知道
askQuestion
可以在外部作用域中访问,但是标识符呢ofTheTeacher
?ofTheTeacher
在函数内部被声明为标识符:
var askQuestion = function ofTheTeacher() {
console.log(ofTheTeacher);
};
askQuestion();
// function ofTheTeacher()...
console.log(ofTheTeacher);
// ReferenceError: ofTheTeacher is not defined
箭头函数
- 箭头函数的声明方式如下:
var askQuestion = () => {
// ..
};
- 箭头函数不需要单词
function
来定义它。
退出
- 当定义一个函数(声明或表达式)时,就会创建一个新的作用域。作用域的嵌套定位在整个程序中创建了一个自然的作用域层次结构,称为作用域链。
- 每个新的作用域都提供了一个干净的空白区域,用于存放其自身的变量集合。当一个变量名在作用域链的不同层级上重复出现时,就会发生遮蔽,从而阻止从该点向内访问外层变量。
本章到此结束。我很快会带着下一章的笔记回来。
到那时,祝您编码愉快 :)
如果你喜欢阅读这些笔记,或者有任何建议或疑问,欢迎在评论区分享你的观点。
如果你想联系我,请点击以下链接:
LinkedIn | GitHub | Twitter | Medium
鏂囩珷鏉ユ簮锛�https://dev.to/rajat2502/the-scope-chain-in-javascript-596o