JavaScript 中的作用域和提升
先决条件:了解 Javascript 作用域、Javascript 提升
你知道下面这段代码执行时控制台会打印什么值吗?
Javascript
var x = 10;
function test()
{
var x = 20;
}
test();
console.log(x);
Javascript
var x = 10;
function test()
{
var x = 20;
console.log(x);
}
test();
Javascript
var x = 10;
function test()
{
if (x > 20) {
var x = 50;
}
console.log(x);
}
test();
C
#include
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);
}
Javascript
var x = 10;
console.log(x);
if (true) {
var x = 20;
console.log(x);
}
console.log(x);
Javascript
var x = 10;
console.log(x);
if (true) {
(function() {
var x = 20;
console.log(x);
})();
}
console.log(x);
Javascript
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);
Javascript
var x = 10;
function test()
{
if (x > 20) {
var x = 50;
}
console.log(x);
}
test();
Javascript
var x;
x = 10;
function test()
{
var x;
if (x > 20) {
x = 50;
}
console.log(x);
}
test();
Javascript
function test()
{
if (false) {
var x = 50;
}
console.log(x);
console.log(y);
var y = 100;
console.log(y);
}
test();
Javascript
function test()
{
var x, y;
if (false) {
x = 50;
}
console.log(x);
console.log(y);
y = 100;
console.log(y);
}
test();
Javascript
function test()
{
if (false) {
let x = 50;
}
console.log(x);
console.log(y);
let y = 100;
console.log(y);
}
test();
Javascript
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();
Javascript
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();
如果你的答案是 10,那么你是对的。在这里,在函数“test”之外声明(当然也是初始化)的变量“x”具有全局范围,这就是为什么它可以在这个范围内的任何地方全局访问。但是,在“测试”函数内声明和初始化的那个只能在该函数内访问。因此,下面的代码片段将在执行时在控制台上打印 20。
Javascript
var x = 10;
function test()
{
var x = 20;
console.log(x);
}
test();
现在尝试猜测这个的输出。
Javascript
var x = 10;
function test()
{
if (x > 20) {
var x = 50;
}
console.log(x);
}
test();
如果您按照前面示例中讨论的范围逻辑猜到了 10,那么您很不幸,因为 10 不是正确答案。这次它将在控制台上打印“未定义”。这是因为变量作用域和变量提升的综合效应。
JavaScript 中的作用域
让我们首先了解作用域。范围是可以访问变量的程序区域。换句话说,范围决定了变量的可访问性/可见性。由于 JavaScript 看起来像 C 系列语言,很明显认为 JavaScript 中的作用域与大多数后端编程语言(如 C、C++ 或Java)中的作用域相似。
让我们考虑以下用 C 编写的代码片段:
C
#include
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 系列的其余部分具有block-level-scope 。每当控件进入一个块,例如 if-block 或循环(例如,for、while 或 do-while)时,该语言允许声明只能在该块内可见的新变量。这里内部作用域中声明的变量不会影响外部作用域中声明的变量的值。但在 JavaScript 中并非如此。
让我们考虑以下 JavaScript 代码:
Javascript
var x = 10;
console.log(x);
if (true) {
var x = 20;
console.log(x);
}
console.log(x);
输出 :
10
20
20
与前面用 C 编写的代码不同,这里最后的输出会根据分配给 if 块内变量“x”的值而变化。这是因为 JavaScript 没有块级作用域。它具有功能级范围。这意味着 if 语句和循环等块不会在 JavaScript 中创建新的范围。而是仅在定义函数时才创建新范围。这有时会给使用 C、C++、C# 或类 Java 语言的开发人员造成混淆。幸运的是,JavaScript 允许函数定义进入任何块内。例如,我们可以通过在 if 块中实现IIFE(立即调用函数表达式)来编写上述代码,这将导致不同的输出。
Javascript
var x = 10;
console.log(x);
if (true) {
(function() {
var x = 20;
console.log(x);
})();
}
console.log(x);
输出 :
10
20
10
在这种情况下,当控件进入在 if 块中定义和调用的匿名函数时,它会创建一个新的作用域。在此范围内声明的变量“x”不会影响在外部范围内声明的变量“x”的值。尽管这是一种在需要时创建临时作用域的非常灵活的方法,但它不符合良好的编码风格。因此,为了简单起见,ES6 引入了两个新的关键字——'let' 和 'const',来声明块作用域的变量。当使用 'const' 或 'let' 声明变量时,它仅在声明它的特定块内可见。例如:
Javascript
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);
输出 :
10
20
30
20
10
在这种情况下,当调用test()函数时,在 if-block 中编写的console.log()语句在控制台上打印 30,而在 test()函数内 if-block 后面的语句在控制台上打印 20 .这意味着使用let关键字声明和定义的变量 'x' 对在其范围之外声明的变量 'x' 的值没有影响,即 if 块。关键字“const”也以类似的方式运行。 'let' 和 'const' 之间的唯一区别是 - const是一个不会重新分配标识符的信号(这里首选单词 'identifier' 而不是 'variable' 的原因是因为 'const' 用于声明标识符,它不再是一个变量。它是一个常数。),而let是一个信号,表明该变量可以被重新分配。
现在让我们回到本文前面提供的以下示例。
Javascript
var x = 10;
function test()
{
if (x > 20) {
var x = 50;
}
console.log(x);
}
test();
输出 :
undefined
我们已经讨论了为什么在这个例子中console.log()语句没有在控制台上打印 10。由于 JavaScript(尤其是var关键字)具有函数级作用域,因此在 if 块中声明的变量“x”在整个函数test() 中都是可见的。因此,当console.log()语句执行时,它会尝试打印内部 'x' 的值,而不是函数定义之外声明的值。现在的问题是,如果这个代码片段打印了内部“x”的值,那么为什么它会打印“未定义”?在这里,内部的“x”在 if 块中被声明和定义,该块被评估为 false,并且在 JavaScript 中尝试在声明导致ReferenceError之前访问变量。那么为什么变量甚至在函数内部被声明,允许它在没有任何错误的情况下执行呢?函数级范围是否使条件语句(如 if-else)无效,如果是,为什么不打印 50,即函数内部声明的 'x' 的实际值?所有这些问题都有一个答案——提升。
Javascript中的提升
提升是 JavaScript 将声明移动到其包含范围顶部的默认行为。当解释 JavaScript 代码时,解释器会无形地将所有变量和函数声明移动(提升)到声明它们的范围的顶部。但是,它们的定义/初始化/实例化的位置不受影响。例如,上面的代码片段在执行之前将被解释为以下内容。
Javascript
var x;
x = 10;
function test()
{
var x;
if (x > 20) {
x = 50;
}
console.log(x);
}
test();
在这种情况下,由于没有为 test()函数顶部声明的变量“x”赋值,JavaScript 会自动为其赋值“未定义”。由于 if 条件被评估为假,“console.log()”语句在控制台上打印“未定义”。
现在让我们看另一个例子:
Javascript
function test()
{
if (false) {
var x = 50;
}
console.log(x);
console.log(y);
var y = 100;
console.log(y);
}
test();
输出:
undefined
undefined
100
此代码解释如下。
Javascript
function test()
{
var x, y;
if (false) {
x = 50;
}
console.log(x);
console.log(y);
y = 100;
console.log(y);
}
test();
在解释代码中声明“x”和“y”的函数主体的第一行中,JavaScript 将“未定义”分配给这两个变量。由于 if 条件被评估为假,前两个“console.log()”语句在控制台上打印“未定义”。但是,最后一条语句打印 100,因为它在最终的 'console.log()' 执行之前被分配给 'y'。
现在重要的一点是,这种变量提升机制仅适用于使用var关键字声明的变量。它不适用于分别使用let和const关键字声明的变量或标识符。让我们考虑以下示例。
Javascript
function test()
{
if (false) {
let x = 50;
}
console.log(x);
console.log(y);
let y = 100;
console.log(y);
}
test();
输出:
ReferenceError: x is not defined
使用let或const声明的标识符根本不会被提升。这使得它们在原始源代码中声明之前无法访问。
Now a question may come- what is the best practice to declare variables and constants in JavaScript keeping variable hoisting in mind, so that it will never make our code return an unexpected result?
Here is the answer.
- All variables and constants that need to be visible within the scope of a particular function should be declared using ‘var’ and ‘const’ keywords respectively at the top of that function.
- Inside blocks (conditional statements or loops) variables and constants should be declared on the top of the block using ‘let’ and ‘const’ respectively.
- If in a particular scope multiple variables or constants need to be declared, then declare them in one go by using a single ‘var’ or ‘let’ or ‘const’ keyword with comma separated identifier names,
e.g.,
var x, y, z; // declaring function-scoped variables
let a, b, c; // declaring block-scoped variables
const u, v, w; // declaring block-scoped constants
Though the last point has nothing to do with the consequences of variable hoisting, it is a better practice to keep it in mind while writing code to keep the code clean.
像变量一样,函数也在 JavaScript 中被提升。但是,如何进行提升取决于函数的声明方式。 JavaScript 允许开发人员以两种方式定义函数——函数声明和函数表达式。
让我们考虑以下示例。
Javascript
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();
输出:
foo
TypeError: bar is not a function
JavaScript 解释器将上述代码解释如下。
Javascript
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();
由于函数foo() 是使用函数声明定义的,因此 JavaScript 解释器将其声明移动到其容器范围的顶部,即 test() 的主体,而将其定义部分留在后面。调用 foo() 时动态分配定义。这导致在调用函数时执行 foo()函数的主体。另一方面,使用函数表达式定义函数只不过是变量初始化,其中函数被视为要分配给变量的值。因此,它遵循适用于变量声明的相同提升规则。这里变量'bar'的声明被移动到它的容器范围的顶部,而分配给它的函数的位置保持不变。 JavaScript 不能将变量解释为函数,除非它被赋予一个实际上是函数的值。这就是为什么尝试执行语句bar();在定义 'bar' 之前会导致TypeError 。
Though the decision regarding which way one should use to define a function solely depends on developer’s own choice, it is better to keep the following things in mind.
- Define all the functions using function declaration method on the top of their container scope. This not only keeps the code clean but also ensures that all the functions are declared and defined before they are invoked.
- If you must have to define a function using function expression method, then make sure that it is defined before it is invoked in the code. This will eliminate the chance of unexpected outputs or errors that may result due to hoisting.