理解JavaScript中的变量、范围和Hoisting
变量是任何编程语言的基本模块之一,每一种语言定义我们如何声明和与变量互动的方式,都会使一种编程语言成败与否。因此,任何开发人员都需要了解如何有效地使用变量、其规则和特性。在今天的教程中,我们将学习如何在JavaScript中声明、交互和定位变量。我们将介绍新的概念和重要的JavaScript关键字,如var 、let 、const 。
声明变量
现在,JavaScript有三个不同的关键字来声明变量:var 、let 和const 。每一个都有自己的属性和特点。让我们先对这三个关键字做一个简单的比较表,然后再详细介绍一下:
| 关键字 | 范围 | 悬挂 | 可以被重新分配 |
|---|---|---|---|
| 变量 | 功能 | 是 | 是的 |
| 让 | 块 | 不 | 是的 |
| 构成 | 块 | 没有 | 没有 |
如果你现在还不清楚我们所说的范围、提升或其他属性是什么意思,请不要担心。我们接下来会详细介绍它们。
变量范围
范围在JavaScript中指的是代码的上下文(或部分),它决定了变量的可访问性(可见性)。在JavaScript中,我们有2种类型的范围,本地和全局。尽管局部范围可以有不同的含义。
让我们通过举一些例子来说明作用域的定义。假设你定义了一个变量message:
const message = 'Hello World'
console.log(message) // 'Hello World'
正如你所期望的那样,在console.log 中使用的变量message 将会存在,其值为Hello World 。毫无疑问,但如果我改变一下声明变量的位置,会发生什么呢?
if (true) {
const message = 'Hello World'
}
console.log(message) // ReferenceError: message is not defined
Ups......看起来我们破坏了它,但为什么?事情是这样的:if 语句创建了一个本地块范围,由于我们使用了const,所以变量只为这个块范围声明,不能从外部访问。
让我们再来讨论一下块作用域和函数作用域。
块作用域
一个块基本上是一段代码(0个或更多的语句),它由一对大括号划定,并可以有选择地进行标注。
正如我们已经讨论过的,使用let 和const ,我们可以定义在块范围内的变量。接下来,我们将通过使用不同的关键字来生成新的作用域来构建非常类似的例子:
const x1 = 1
{
const x1 = 2
console.log(x1) // 2
}
console.log(x1) // 1
让我们解释一下这个,因为它一开始可能看起来有点奇怪。在我们的外层作用域中,我们定义了变量x1 ,其值为1 。然后我们通过简单地使用大括号来创建一个新的块作用域,这很奇怪,但在JavaScript中是完全合法的,在这个新的作用域中,我们创建了一个新的变量(与外层作用域中的变量分开),也命名为x1 。但是不要混淆,这是一个全新的变量,它只在这个作用域内可用。
同样的例子,现在有一个命名的作用域:
const x2 = 1
myNewScope: { // Named scope
const x2 = 2
console.log(x2) // 2
}
console.log(x2) // 1
虽然这个例子**(不要运行下面的代码!!!!!!!!!!!!!!!!** ):
const x3 = 1
while(x3 === 1) {
const x3 = 2
console.log(x3) // 2
}
console.log(x3) // Never executed
你能猜到这段代码有什么问题吗?如果你运行它又会发生什么?...让我解释一下,在外层作用域中声明的x3 是用来进行while比较的x3 === 1 ,通常在while语句中,我可以给x3 重新赋值并退出循环,但是由于我们在块作用域中声明了一个新的x3 ,我们不能再从外层作用域中改变x3 ,因此while条件将总是评估为true ,产生一个无限的循环,会挂起你的浏览器,或者如果你在NodeJS上使用终端来运行它,会打印很多的2 。
修复这个特殊的代码可能很棘手,除非你真的重新命名这两个变量。
到目前为止,在我们的例子中,我们使用了const ,但完全相同的行为会发生在let 。然而,我们在比较表中看到,关键字var 实际上是函数范围,那么它对我们的例子意味着什么呢?嗯......让我们来看看。
var x4 = 1
{
var x4 = 2
console.log(x4) // 2
}
console.log(x4) // 2
令人惊奇的是!尽管我们在作用域内重新声明了x4 ,但它在内部作用域和外部作用域上都将值改为2 。这是在let,const, 和var 之间最重要的区别之一,通常是面试问题的主题(以这种或那种方式)。
功能范围
一个函数作用域在某种程度上也是一个块作用域,所以let 和const 的行为与我们前面的例子中的相同。然而,函数作用域也封装了用var 声明的变量。但让我们继续看我们的xn 例子。
const 或 的例子let
const x5 = 1
function myFunction() {
const x5 = 2
console.log(x5) // 2
}
myFunction()
console.log(x5) // 1
和我们预期的完全一样,现在用var
var x6 = 1
function myFunction() {
var x6 = 2
console.log(x6) // 2
}
myFunction()
console.log(x6) // 1
在这种情况下,var的工作方式与let 和const 相同此外:
function myFunction() {
var x7 = 1
}
console.log(x7) // ReferenceError: x7 is not defined
正如我们所看到的,var 声明只存在于它们所创建的函数中,不能从外部访问。
但是还有更多的东西,因为JS一直在发展,更新的作用域类型已经被创造出来。
模块范围
随着ES6中模块的引入,一个模块中的变量不能直接影响其他模块中的变量,这一点很重要。你能想象一个从库中导入模块会与你的变量发生冲突的世界吗?即使是JS也没有那么混乱!所以根据定义,模块创建了自己的作用域,封装了所有用var,let 或const 创建的变量,类似于函数作用域。
不过,模块提供了一些导出变量的方法,这样它们就可以从模块外部访问,这一点我已经在《JavaScript模块介绍》一文中讲过了。
到目前为止,我们已经讨论了不同类型的局部作用域,现在让我们深入讨论全局作用域。
全局范围
在任何函数、块或模块范围之外定义的变量具有全局作用。全局作用域中的变量可以从应用程序的任何地方被访问。
全局范围有时会与模块范围相混淆,但事实并非如此,全局范围的变量可以跨模块使用,尽管这被认为是一种不好的做法,而且有充分的理由。
你会如何去声明一个全局变量呢?这取决于上下文,在浏览器上与NodeJS应用程序是不同的。在浏览器的上下文中,你可以做一些简单的事情:
<script>
let MESSAGE = 'Hello World'
console.log(MESSAGE)
</script>
或者通过使用窗口对象:
<script>
window.MESSAGE = 'Hello World'
console.log(MESSAGE)
</script>
有一些原因你想做这样的事情,然而,当你这样做的时候,一定要小心。
嵌套作用域
你现在可能已经猜到了,可以嵌套作用域,意思是在另一个作用域中创建一个作用域,这是一种非常普遍的做法。只要在一个函数中添加一个if 语句,我们就可以做到这一点。所以让我们看一个例子:
function nextedScopes() {
const message = 'Hello World!'
if (true) {
const fromIf = 'Hello If Block!'
console.log(message) // Hello World!
}
console.log(fromIf) // ReferenceError: fromIf is not defined
}
nextedScopes()
词法范围
在某种程度上,我们已经使用了词法范围,尽管我们并不了解它。词法范围仅仅意味着子作用域可以访问外作用域中定义的变量。
让我们通过一个例子来看看:
function outerScope() {
var name = 'Juan'
function innerScope() {
console.log(name) // 'Juan'
}
return innerScope
}
const inner = outerScope()
inner()
这看起来比实际情况更奇怪,所以我们来解释一下。函数outerScope 声明了一个变量name ,其值为Juan ,还有一个名为innerScope 的函数。后者没有为自己的作用域声明任何变量,而是利用了外部函数作用域中声明的变量name 。
当outerScope() 被调用时,它返回一个对innerScope 函数的引用,这个函数后来被从最外层的作用域调用。Juan 当第一次阅读这段代码时,你可能会感到困惑,为什么innerScope 会有console.log 的值,因为我们是从全局作用域或模块作用域中调用它,而那里没有声明name 。
这样做的原因是由于JavaScript的闭包。闭包是一个独立的话题,你可以在MDN文档中阅读更多关于它的信息。我正在计划写一篇文章,用简单的术语解释闭包,但在写这篇文章的时候还没有准备好。
悬挂
就JavaScript而言,Hoisting是指在编译阶段在内存中创建一个变量,因此它们在实际声明之前就可以被使用。听起来超级令人困惑,让我们最好在代码中看到它。
这就是正常流程的样子:
function displayName(name) {
console.log(name)
}
displayName('Juan')
//***********************
// Outputs
//***********************
// 'Juan'
真棒!正如预期的那样,这可以工作,但你会怎么想下面的情况:
hoistedDisplayName('Juan')
function hoistedDisplayName(name) {
console.log(name)
}
//***********************
// Outputs
//***********************
// 'Juan'
等等等等....,什么?虽然听起来很疯狂,但由于函数在代码实际运行之前就被分配到了内存中,所以函数hoistedDisplayName ,在其实际定义之前就可以使用,至少在代码行方面是如此。
函数有这种特殊的属性,但用var 声明的变量也是如此。让我们看一个例子:
console.log(x8) // undefined
var x8 = 'Hello World!'
不是你猜的那样吗?变量在代码中的实际定义之前被 "创建",并不意味着它的值已经被分配了,这就是为什么当我们做console.log(x8) ,我们不会得到一个错误,说这个变量没有被声明,而是说这个变量有值undefined 。非常有趣,但如果我们使用let 或const 会发生什么?记得在我们的表格中,它们并不共享这个属性:
console.log(x9) // Cannot access 'x9' before initialization
const x9 = 'Hello World!'
它抛出了一个错误。
悬挂是JavaScript变量的一个不太为人所知的属性,但它也是一个重要的属性。请确保你了解其中的区别,它对你的代码很重要,而且可能是面试问题的一个话题。
变量的重新分配
这个话题特别涵盖了用关键字const 声明的变量。用const 声明的变量不能被重新赋值,也就是说,我们不能为一个新的变量改变它的值,但是有一个技巧。让我们看看一些例子:
const c1 = 'hello world!'
c1 = 'Hello World' // TypeError: Assignment to constant variable.
正如我们所料,我们不能改变一个常数的值,或者说我们可以吗?
const c2 = { name: 'Juan' }
console.log(c2.name) // 'Juan'
c2.name = 'Gera'
console.log(c2.name) // 'Gera'
我们刚刚改变了一个const 的值吗?简短的回答是NO。我们的常量c2 引用了一个有属性name 的对象。c2 是对该对象的引用,这是它的值。当我们做c2.name 的时候,我们实际上是拿着指向c2 对象的指针并从那里访问该属性。当我们做c2.name ,我们所改变的是对象中的属性值name ,而不是存储在c2 的引用,因此c2 保持不变,尽管现在的属性值不同。
看看当我们实际尝试以不同的方式更新该值时会发生什么。
const c3 = { name: 'Juan' }
console.log(c3.name) // 'Juan'
c3 = { name: 'Gera' } // TypeError: Assignment to constant variable.
console.log(c3.name)
尽管对象看起来是一样的,但我们实际上是在创建一个新的对象{ name: 'Gera' } ,并试图将这个新对象分配给c3 ,但我们不能这样做,因为它被声明为常量。
总结
今天我讲述了JavaScript中的变量声明和范围的话题。这是一个非常重要的话题,可以解释许多可能发生在我们代码中的奇怪情况。而这也是一个常见的面试问题。对于所有的JavaScript开发人员来说,这是一个必须学习和理解的问题。
前段时间,我发表了一篇关于测试你技能的5个JavaScript问题和答案的文章,其中有两个问题(#4和#5)是实际的面试问题。整篇文章非常有趣,但这2个问题尤其是很好地说明了作用域和闭包是如何对你的代码结果产生很大影响的。
非常感谢您的阅读!