\
您当前的位置 : 首页 >> 帮助中心

理解JavaScript中的作用域

来源:恩氏滤油机
时间:2024-09-23
浏览量:1004

范围,或确定变量所在位置的一组规则,是任何编程语言的最基本概念之一。事实上,它是如此基本,以至于我们很容易忘记这些规则是多么微妙!

准确理解 JavaScript 引擎如何“思考”作用域将使您避免编写提升可能导致的常见错误,让您做好准备专注于闭包,并让您离永远不再编写错误更近一步 再次。

...无论如何,它会帮助您理解提升和关闭。

在本文中,我们将了解:

立即学习“Java免费学习笔记(深入)”;

JavaScript 中作用域的基础知识

解释器如何决定哪些变量属于哪个作用域

吊装的实际工作原理

ES6 关键字 let 和 const 如何改变游戏规则

让我们深入探讨。

如果您有兴趣了解有关 ES6 的更多信息以及如何利用语法和功能来改进和简化 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 保存到 class="inline">popup —到全局范围 — 并将其值设置为 undefined。

由于函数内不再有变量声明,编译器将退回到全局作用域。由于不再有变量声明,因此此阶段已完成。

请注意,我们实际上还没有运行任何东西。此时编译器的工作只是确保它知道每个人的名字;它不关心他们做什么。

此时,我们的程序知道:

全局范围内有一个名为 first_name 的变量。

全局范围内有一个名为 popup 的函数。

在 popup 范围内有一个名为 last_name 的变量。

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。

您可以在定义之前调用 已损坏的,但您会收到 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。

letconst 和临时死区

与 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 文档。

词法 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中的作用域的详细内容,更多请关注本网内其它相关文章!

免责申明

以上展示内容来源于合作媒体、企业机构、网友提供或网络收集整理,版权争议与本站无关,文章涉及见解与观点不代表恩氏滤油机网官方立场,请读者仅做参考。本文欢迎转载,转载请说明出处。若您认为本文侵犯了您的版权信息,或您发现该内容有任何涉及有违公德、触犯法律等违法信息,请您立即联系我们及时修正或删除。
Copyright © 2004-2026 BaiJiaMai.Com 重庆恩氏过滤设备制造有限公司 版权所有  网站备案号:渝ICP备2024041059号