MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript变量作用域与提升机制

2022-05-053.2k 阅读

JavaScript变量作用域

在JavaScript编程中,变量作用域是一个核心概念,它决定了变量的可访问性和生命周期。理解变量作用域对于编写高效、可靠的JavaScript代码至关重要。

全局作用域

全局作用域是JavaScript中最外层的作用域。在全局作用域中声明的变量,在整个脚本的任何地方都可以访问。例如:

var globalVar = 'I am a global variable';
function printGlobalVar() {
    console.log(globalVar);
}
printGlobalVar(); 

在上述代码中,globalVar 是在全局作用域中声明的变量,printGlobalVar 函数可以在其内部访问该变量。

在浏览器环境中,全局作用域下声明的变量会成为 window 对象的属性(在严格模式下略有不同,后面会提及)。例如:

var globalInWindow = 'In window';
console.log(window.globalInWindow); 

然而,过度使用全局变量并不是一个好的编程习惯。因为全局变量可以在任何地方被修改,这可能导致命名冲突,使得代码难以维护和调试。

函数作用域

函数作用域是指变量在函数内部声明时,其作用域仅限于该函数内部。函数内部声明的变量在函数外部是无法访问的。例如:

function funcScope() {
    var localVar = 'I am local to this function';
    console.log(localVar);
}
funcScope();
console.log(localVar); 

在上述代码中,localVar 是在 funcScope 函数内部声明的变量,在函数外部访问 localVar 会导致 ReferenceError,因为 localVar 的作用域仅限于 funcScope 函数内部。

函数作用域有一个重要的特性,即函数内部可以访问外部作用域的变量,但外部作用域无法访问函数内部的变量。这种特性形成了一种作用域链的概念。例如:

var outerVar = 'I am outer';
function innerFunc() {
    console.log(outerVar); 
    var innerVar = 'I am inner';
}
innerFunc();
console.log(innerVar); 

innerFunc 函数内部,可以访问 outerVar,因为函数内部的作用域链会向上查找外部作用域的变量。但在函数外部访问 innerVar 会引发错误。

块级作用域(ES6之前)

在ES6之前,JavaScript并没有真正的块级作用域。像 ifforwhile 等块级结构不会创建新的作用域。例如:

if (true) {
    var blockVar = 'I am in a block';
}
console.log(blockVar); 

在上述代码中,blockVar 虽然是在 if 块内部声明的,但由于JavaScript在ES6之前没有块级作用域,blockVar 实际上具有函数作用域(如果在全局作用域内,就是全局作用域)。所以在 if 块外部仍然可以访问 blockVar

同样,for 循环中的变量声明也存在类似情况:

for (var i = 0; i < 5; i++) {
    console.log(i);
}
console.log(i); 

这里的 ifor 循环结束后仍然可以访问,这可能会导致一些意想不到的错误。

块级作用域(ES6之后 - let和const)

ES6引入了 letconst 关键字,它们用于创建块级作用域。let 声明的变量具有块级作用域,只在其所在的块({})内有效。例如:

if (true) {
    let blockLetVar = 'I am a let variable in block';
    console.log(blockLetVar);
}
console.log(blockLetVar); 

在上述代码中,在 if 块外部访问 blockLetVar 会导致 ReferenceError,因为 blockLetVar 的作用域仅限于 if 块内部。

const 声明的常量同样具有块级作用域:

if (true) {
    const blockConstVar = 'I am a const variable in block';
    console.log(blockConstVar);
}
console.log(blockConstVar); 

if 块外部访问 blockConstVar 也会引发 ReferenceError

for 循环中使用 let 时,每次迭代都会创建一个新的块级作用域:

for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i); 
    }, 1000);
}
console.log(i); 

在上述代码中,setTimeout 回调函数中输出的是正确的迭代值,因为 let 在每次迭代时创建了新的块级作用域,每个 i 都有自己独立的作用域。而在 for 循环外部访问 i 会导致 ReferenceError

作用域链

当JavaScript引擎需要查找一个变量时,它会从当前作用域开始,沿着作用域链向上查找,直到找到该变量或者到达全局作用域。如果在全局作用域也没有找到该变量,则会抛出 ReferenceError

例如:

var outer = 'outer variable';
function middle() {
    var middleVar ='middle variable';
    function inner() {
        var innerVar = 'inner variable';
        console.log(outer); 
        console.log(middleVar); 
        console.log(innerVar); 
    }
    inner();
}
middle();

inner 函数中,首先在自身作用域查找变量,找到了 innerVar。对于 outermiddleVar,在自身作用域未找到,于是沿着作用域链向上查找,分别在全局作用域和 middle 函数作用域找到了相应变量。

作用域链的形成与函数的定义和调用密切相关。每个函数在创建时,都会关联到它的外部作用域,这个外部作用域就是函数定义时所在的作用域。当函数被调用时,就会形成一个基于这个外部作用域的作用域链。

例如:

function outerFunc() {
    var outerVar = 'outer in outerFunc';
    function innerFunc() {
        console.log(outerVar); 
    }
    return innerFunc;
}
var funcRef = outerFunc();
funcRef(); 

在上述代码中,innerFunc 在定义时,其外部作用域是 outerFunc 的作用域。当 outerFunc 返回 innerFunc 并被赋值给 funcRef 后,funcRef 调用时仍然可以访问到 outerFunc 作用域中的 outerVar,因为作用域链是基于函数定义时的外部作用域形成的,而不是调用时的作用域。

JavaScript变量提升机制

变量提升是JavaScript中一个独特且重要的概念。它指的是变量声明在其作用域内会被提升到顶部,但变量的赋值不会被提升。

变量提升的原理

在JavaScript代码执行之前,引擎会先进行词法分析和预编译。在预编译阶段,变量声明会被提升到其所在作用域的顶部。例如:

console.log(hoistedVar); 
var hoistedVar = 'I am hoisted';

按照正常的代码顺序,在 console.log(hoistedVar) 时,hoistedVar 应该还未声明。但由于变量提升,var hoistedVar 声明被提升到了作用域顶部,此时 hoistedVar 已经声明,但还未赋值,所以输出 undefined。实际上,上述代码等价于:

var hoistedVar;
console.log(hoistedVar); 
hoistedVar = 'I am hoisted';

函数作用域内同样存在变量提升。例如:

function varHoistInFunc() {
    console.log(funcVar); 
    var funcVar = 'I am in function';
}
varHoistInFunc();

varHoistInFunc 函数内部,var funcVar 声明被提升到函数作用域顶部,所以先输出 undefined,然后再进行赋值。

函数声明提升

不仅变量声明会提升,函数声明同样会被提升。而且函数声明的提升优先级高于变量声明。例如:

func(); 
function func() {
    console.log('I am a hoisted function');
}

上述代码可以正常执行并输出 I am a hoisted function,因为函数声明 func 被提升到了作用域顶部。

当变量声明和函数声明同名时,函数声明提升优先级更高。例如:

console.log(func); 
function func() {
    console.log('Function');
}
var func = 'Variable';

这里输出的是函数定义,而不是 undefined。因为函数声明先被提升,然后变量声明 var func 虽然也被提升,但它不会覆盖已经提升的函数声明。不过,如果对 func 进行赋值操作,函数声明会被覆盖:

func(); 
function func() {
    console.log('Function');
}
var func = 'Variable';
func(); 

第一次调用 func() 输出 Function,第二次调用 func() 会报错,因为 func 已经被赋值为字符串 'Variable',不再是一个函数。

块级作用域中的变量提升(ES6之前)

在ES6之前,块级结构中没有块级作用域,也就不存在真正意义上块级作用域的变量提升。例如:

if (true) {
    console.log(blockVar); 
    var blockVar = 'In block';
}

这里 blockVar 具有函数作用域(如果在全局作用域内,就是全局作用域),var blockVar 声明被提升到函数或全局作用域顶部,所以输出 undefined

块级作用域中的变量提升(ES6之后 - let和const)

ES6引入的 letconst 虽然也存在变量提升,但与 var 有很大不同。它们存在所谓的“暂时性死区”(TDZ)。例如:

console.log(letVar); 
let letVar = 'I am a let variable';

上述代码会抛出 ReferenceError,因为 let 声明的变量虽然被提升,但在声明语句之前访问会进入暂时性死区。只有执行到 let letVar 声明语句时,变量才正式被初始化并可以使用。

const 同样存在暂时性死区:

console.log(constVar); 
const constVar = 'I am a const variable';

同样会抛出 ReferenceError,因为在声明语句之前访问 constVar 进入了暂时性死区。

在块级作用域内,letconst 变量的提升使得代码逻辑更加清晰,避免了一些因变量提升导致的意外行为。例如:

{
    let localVar = 'Initial value';
    {
        console.log(localVar); 
        let localVar = 'New value';
    }
}

这里内部块级作用域中的 let localVar 声明创建了一个新的块级作用域,虽然与外部块级作用域中的 localVar 同名,但由于暂时性死区,在声明语句之前访问会报错,而不是访问到外部的 localVar。这有助于避免变量命名冲突和意外的变量覆盖。

提升机制与作用域的结合

理解变量提升和作用域的结合对于编写正确的JavaScript代码至关重要。例如,在函数嵌套的情况下:

function outer() {
    var outerVar = 'Outer variable';
    function inner() {
        console.log(outerVar); 
        var innerVar = 'Inner variable';
        console.log(innerVar); 
    }
    inner();
    console.log(innerVar); 
}
outer();

inner 函数中,console.log(outerVar) 能够访问到外部作用域的 outerVar,这是作用域链的体现。而 innerVar 先被提升到 inner 函数作用域顶部,所以在 console.log(innerVar) 时输出 undefined,然后再进行赋值。在 outer 函数中访问 innerVar 会报错,因为 innerVar 的作用域仅限于 inner 函数内部。

再看一个结合 letvar 的复杂例子:

function complexScope() {
    var outerVar = 'Outer var';
    {
        let blockLet = 'Block let';
        console.log(outerVar); 
        console.log(blockLet); 
        {
            var innerVar = 'Inner var';
            let innerLet = 'Inner let';
            console.log(outerVar); 
            console.log(blockLet); 
            console.log(innerVar); 
            console.log(innerLet); 
        }
        console.log(innerVar); 
        console.log(innerLet); 
    }
}
complexScope();

在这个例子中,不同作用域的变量提升和作用域链相互作用。outerVarvar 声明的,具有函数作用域,在各个块级作用域内都可以访问。blockLetlet 声明的,具有块级作用域,在其所在块级作用域及其内部块级作用域可以访问。innerVarvar 声明的,在包含它的函数作用域内都可以访问。innerLetlet 声明的,只在其所在的内部块级作用域内有效。在外部块级作用域访问 innerLet 会报错,而访问 innerVar 可以正常输出,这展示了 varlet 在作用域和变量提升方面的差异。

通过深入理解JavaScript变量作用域与提升机制,开发者能够更好地掌控代码的执行逻辑,避免因变量作用域混乱和变量提升带来的错误,编写出更加健壮、易于维护的JavaScript程序。无论是开发大型应用程序还是小型脚本,这些知识都是JavaScript编程的基石。