JavaScript 作用域和变量提升
先决条件: 理解JavaScript的作用域和变量提升
在JavaScript中,有两种类型的作用域:
- 全局作用域: 指的是附加在窗口上最外层函数之外的作用域。
- 局部作用域: 指的是正在执行的函数内部的作用域。
变量提升: 是一种概念,它使我们能够在初始化/赋值之前提取变量和函数的值,而不会出现错误,这是由于执行上下文的第一个阶段(内存创建阶段)导致的。
当执行以下代码片段时,你知道控制台会打印什么值吗?
<script>
var x = 10;
function test()
{
var x = 20;
}
test();
console.log(x);
</script>
输出:
10
如果你的答案是10,那么你是对的。在函数’test’之外声明的变量’x’具有全局作用域,因此在全局范围内可以随处访问它。然而,在’test’函数内部声明和初始化的变量只能在该函数内部访问。
因此,执行下面的代码片段将在控制台上打印出20。
<script>
var x = 10;
function test()
{
var x = 20;
console.log(x);
}
test();
</script>
输出:
20
<script>
var x = 10;
function test()
{
if (x > 20) {
var x = 50;
}
console.log(x);
}
test();
</script>
输出:
undefined
如果你根据之前讨论的作用域逻辑猜测为10,那么你是不幸的,因为10不是正确的答案。这次它会在控制台上打印’undefined’。这是因为变量的作用域和变量提升的综合效应。
JavaScript的作用域: 让我们先理解作用域。 作用域 是程序中一个变量可以被访问的区域。换句话说,作用域决定了变量的可访问性/可见性。由于JavaScript看起来像一种类C语言,很显然会认为JavaScript中的作用域与大多数后端编程语言(如C、C++或Java)中的作用域类似。
让我们考虑以下用C语言编写的代码片段:
#include<stdio.h>
void doSomething();
int main() {
doSomething();
return 0;
}
// This function examplifies
// the block-level-scope in C language
void doSomething() {
int x = 10;
printf("%d\n", x);
if (x == 10) {
int x = 20;
printf("%d\n", x);
}
printf("%d\n", x);
}
输出:
10
20
10
在这里输出是这样的,因为C语言以及C系列的其他语言具有 块级作用域 。每当控制进入一个块中,比如if块或循环块(例如for、while或do-while),语言允许声明新的变量,这些变量只在该块中可见。在这里,内部作用域中声明的变量不会影响外部作用域中声明的变量的值。但是,在JavaScript中不是这样的。
示例: 考虑下面的JavaScript示例。
<script>
var x = 10;
console.log(x);
if (true) {
var x = 20;
console.log(x);
}
console.log(x);
</script>
输出:
10
20
20
与前面的C语言代码不同,这里的最后输出结果会根据在if语句块内赋给变量’x’的值而变化。这是因为JavaScript没有块级作用域。它有一个 函数级作用域 。这意味着if语句和循环等块在JavaScript中不会创建新的作用域。只有在定义函数时才会创建新的作用域。这有时会使那些使用C、C++、C#或类Java语言的开发人员感到困惑。幸运的是,JavaScript允许将函数定义放在任何块内。例如,我们可以通过在if块内部实现一个 IIFE(立即调用的函数表达式) 来编写上述代码,以得到不同的输出结果。
<script>
var x = 10;
console.log(x);
if (true) {
(function() {
var x = 20;
console.log(x);
})();
}
console.log(x);
</script>
输出:
10
20
10
在这种情况下,当控制流进入在if块内定义和调用的匿名函数时,它会创建一个新的作用域。在此作用域中声明的变量“x”不会影响外层作用域中声明的变量“x”的值。尽管这是一种在需要临时作用域的地方创建临时作用域的灵活方法,但它不被视为良好的编码风格。因此,为了保持简单,ES6引入了两个新关键字——“let”和“const”,用于声明块级作用域变量。使用“const”或“let”声明的变量仅在其声明的特定块内可见。例如:
<script>
var x = 10;
console.log(x);
function test()
{
var x = 20;
console.log(x);
if (x > 10) {
let x = 30;
console.log(x);
}
console.log(x);
}
test();
console.log(x);
</script>
输出:
10
20
30
20
10
在这种情况下,当 test() 函数被调用时,if块内部的 console.log() 语句在控制台上打印出30,而在test()函数内部的if块后面打印出20。这意味着使用 let 关键字声明和定义的变量’x’对其作用域外部的变量’x’的值没有影响,即if块。关键字’const’也以类似的方式操作。’let’和’const’之间唯一的区别是- const 是一个信号,表明 标识符不会被重新赋值 (这里之所以优先使用’标识符’而不是’变量’,是因为’const’用于声明标识符,它不再是一个变量。它是一个常量。),而 let 是一个信号,表明 变量可以被重新赋值 。
现在让我们回到之前在本文中提供的以下示例。
<script>
var x = 10;
function test()
{
if (x > 20) {
var x = 50;
}
console.log(x);
}
test();
</script>
输出:
undefined
我们已经讨论了为什么在这个示例中 console.log() 语句没有在控制台上打印10。由于JavaScript(特别是 var 关键字)具有函数级作用域,if块内声明的变量’x’在整个test()函数中是可见的。所以当 console.log() 语句执行时,它尝试打印内部’x’的值而不是函数定义外部声明的那个值。现在的问题是,如果这段代码片段打印的是内部’x’的值,那为什么会打印’undefined’呢?这里内部的’x’在if块内部被声明和定义了,但if块的值被计算为false,在JavaScript中,在声明之前尝试访问一个变量会导致 ReferenceError 。那么变量是如何在函数内部被声明,并且能够在没有任何错误的情况下执行的呢?函数级作用域是否使条件语句(如if-else)无效,如果是这样,为什么不打印函数内部声明的实际值50呢?所有这些问题都有一个共同的答案-提升。
JavaScript中的提升: 它是JavaScript的默认行为,将声明的变量提升到其所在作用域的顶部。当JavaScript代码被解释时,解释器会将所有变量和函数声明在它们被声明的作用域的顶部进行不可见的移动(提升)。然而,它们的定义/初始化/实例化的位置不受影响。例如,上面的代码片段在执行之前将被解释为以下内容。
<script>
var x;
x = 10;
function test()
{
var x;
if (x > 20) {
x = 50;
}
console.log(x);
}
test();
</script>
输出:
undefined
在这个示例中,由于在test()函数顶部声明的变量’x’没有被赋值,JavaScript会自动将值’undefined’赋给它。由于if条件被判断为false,’控制台.log()’语句会在控制台上打印’undefined’。 现在让我们看另一个示例:
<script>
function test()
{
if (false) {
var x = 50;
}
console.log(x);
console.log(y);
var y = 100;
console.log(y);
}
test();
</script>
输出:
undefined
undefined
100
这段代码被解释为以下内容。
<script>
function test()
{
var x, y;
if (false) {
x = 50;
}
console.log(x);
console.log(y);
y = 100;
console.log(y);
}
test();
</script>
输出:
undefined
undefined
100
在解释代码中的函数体的第一行,JavaScript将’x’和’y’分别赋值为’undefined’。由于if条件求值为false,前两个’console.log()’语句在控制台上打印出’undefined’。然而,最后一个语句因为在最终的’console.log()’执行之前被赋值为100而打印出100。
现在这里的重要点是,这个变量提升机制只适用于使用 var 关键字声明的变量。它不适用于使用 let 和 const 关键字声明的变量或标识符。让我们看下面的示例。
<script>
function test()
{
if (false) {
let x = 50;
}
console.log(x);
console.log(y);
let y = 100;
console.log(y);
}
test();
</script>
输出:
ReferenceError: x is not defined
使用 let 或 const 声明的标识符根本不会被提升。这使得它们在原始源代码中的声明之前无法访问。
现在可能会有一个问题- 在考虑变量提升的情况下,在JavaScript中声明变量和常量的最佳实践是什么,以便它永远不会使我们的代码返回意外结果 ?
这是答案。
- 需要在特定函数范围内可见的所有变量和常量都应该使用 ‘var’ 和 ‘const’ 关键字在该函数的顶部进行声明。
- 在块(条件语句或循环)内部,变量和常量应该在块的顶部使用 ‘let’ 和 ‘const’ 进行声明。
- 如果在特定的作用域中需要声明多个变量或常量,则可以使用单个’var’或’let’或’const’关键字以逗号分隔的标识符名称来一次性声明它们,例如,var x, y, z; // 声明函数范围的变量 let a, b, c; // 声明块范围的变量 const u, v, w; // 声明块范围的常量
尽管最后一点与变量提升的后果无关,但在编写代码时将其记在心中是一个更好的实践,以保持代码整洁。
与变量类似,JavaScript中的函数也会被提升。然而,提升的方式取决于函数的声明方式。JavaScript允许开发人员以两种方式定义函数-函数声明和函数表达式。
让我们考虑以下示例。
<script>
function test()
{
foo();
bar();
// Function defined
// using function declaration
function foo()
{
console.log('foo');
}
// Function defined
// using function expression
var bar = function() {
console.log('bar');
}
}
test();
</script>
输出:
foo
TypeError: bar is not a function
JavaScript解释器将上面的代码解释如下。
<script>
function test()
{
function foo() {}
var bar;
foo();
bar();
// Function defined
// using function declaration
function foo()
{
console.log('foo');
}
// Function defined
// using function expression
bar = function() {
console.log('bar');
}
}
test();
</script>
输出结果:
foo
TypeError: bar is not a function
由于函数foo()是使用函数声明定义的,JavaScript解释器将其声明移动到其容器范围的顶部,即test()的主体部分,而将其定义部分留在后面。当foo()被调用时,定义是动态分配的。
这导致在调用函数时执行foo()函数的主体。另一方面,使用函数表达式定义函数实际上只是变量初始化,其中函数被视为要分配给变量的值。因此,它遵循适用于变量声明的相同提升规则。在这里,“bar”变量的声明被移动到其容器范围的顶部,而将函数分配给它的位置保持不变。在变量被定义之前,JavaScript无法将变量解释为函数,除非它被分配一个实际上是函数的值。这就是为什么在定义“bar”之前尝试执行语句 bar(); 会导致 TypeError 的原因。
尽管使用哪种方式定义函数的决定完全取决于开发者自己的选择,但最好记住以下几点:
- 在其容器范围的顶部使用 函数声明方法 定义所有函数。这不仅使代码更加清晰,而且确保在调用函数之前,所有函数都已声明和定义。
- 如果必须使用 函数表达式方法 定义函数,请确保在代码中调用函数之前定义它。这将消除由于提升而导致的意外输出或错误的机会。