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

JavaScript中的作用域和变量生命周期

2023-01-056.0k 阅读

JavaScript中的作用域

在JavaScript编程中,作用域是一个至关重要的概念,它决定了变量的可访问性和生命周期。简单来说,作用域定义了变量名在程序中可见的区域。

全局作用域

当在JavaScript代码中定义一个变量而没有将其放在任何函数内部时,这个变量就处于全局作用域中。全局作用域中的变量在整个脚本中都可以访问。例如:

var globalVar = '我是全局变量';
function printGlobalVar() {
    console.log(globalVar);
}
printGlobalVar(); 

在上述代码中,globalVar 是一个全局变量,在 printGlobalVar 函数内部可以直接访问它。

需要注意的是,在浏览器环境下,全局变量会成为全局对象(在浏览器中是 window 对象)的属性。例如:

var globalVar2 = '另一个全局变量';
console.log(window.globalVar2); 

通过 window 对象也可以访问到全局变量 globalVar2

然而,过度使用全局变量会带来一些问题。比如,不同的JavaScript文件可能会定义相同名称的全局变量,从而导致命名冲突。此外,全局变量的生命周期贯穿整个脚本执行过程,可能会占用不必要的内存。

函数作用域

在JavaScript中,函数内部定义的变量具有函数作用域。这意味着这些变量只能在函数内部访问,在函数外部无法访问。例如:

function funcScope() {
    var localVar = '我是函数作用域内的变量';
    console.log(localVar); 
}
funcScope();
console.log(localVar); 

在上述代码中,localVar 变量定义在 funcScope 函数内部,在函数内部可以正常访问并打印它。但在函数外部尝试访问 localVar 时,会抛出 ReferenceError,因为在函数外部 localVar 是未定义的。

函数作用域还有一个特性,就是函数内部可以访问外部作用域的变量,但外部作用域无法访问函数内部的变量。例如:

var outerVar = '我是外部变量';
function accessOuterVar() {
    console.log(outerVar); 
}
accessOuterVar();

accessOuterVar 函数内部可以访问外部的 outerVar 变量并打印它。

块级作用域

在ES6之前,JavaScript没有真正的块级作用域。像 if 语句块、for 循环块等都不会创建新的作用域。例如:

if (true) {
    var blockVar = '我在if块内';
}
console.log(blockVar); 

在上述代码中,blockVar 虽然定义在 if 块内部,但由于没有块级作用域,blockVar 实际上是处于函数作用域(如果在全局代码中,就是全局作用域),所以在 if 块外部依然可以访问到它。

ES6引入了 letconst 关键字,从而实现了块级作用域。使用 letconst 定义的变量,其作用域仅限于当前块。例如:

if (true) {
    let blockLetVar = '我是let定义的块级作用域变量';
    const blockConstVar = '我是const定义的块级作用域常量';
}
console.log(blockLetVar); 
console.log(blockConstVar); 

在上述代码中,在 if 块外部尝试访问 blockLetVarblockConstVar 都会抛出 ReferenceError,因为它们的作用域仅限于 if 块内部。

for 循环中使用 let 也体现了块级作用域的特性。例如:

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

for 循环外部尝试访问 i 会抛出 ReferenceError,因为 i 的作用域仅限于 for 循环块内部。而如果使用 var 定义 i

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

这里在 for 循环外部可以访问到 j,并且其值为 5,这是因为 var 定义的变量没有块级作用域。

作用域链

当在JavaScript中访问一个变量时,引擎会首先在当前作用域中查找该变量。如果在当前作用域中没有找到,它会向上一级作用域查找,以此类推,直到找到该变量或者到达全局作用域。这种由内向外查找变量的链条就称为作用域链。

例如:

var outer = '外部变量';
function outerFunc() {
    var middle = '中间变量';
    function innerFunc() {
        var inner = '内部变量';
        console.log(outer); 
        console.log(middle); 
        console.log(inner); 
    }
    innerFunc();
}
outerFunc();

innerFunc 函数内部,当访问 outermiddleinner 变量时,引擎首先在 innerFunc 的作用域中查找。找到了 inner 变量,所以直接使用。对于 outermiddle 变量,在 innerFunc 作用域中未找到,于是向上一级作用域(即 outerFunc 的作用域)查找,找到了 middle 变量。继续向上查找,在全局作用域中找到了 outer 变量。

再看一个更复杂的例子:

var globalVal = '全局值';
function outerFunction() {
    var outerVal = '外部函数值';
    function middleFunction() {
        var middleVal = '中间函数值';
        function innerFunction() {
            console.log(globalVal); 
            console.log(outerVal); 
            console.log(middleVal); 
        }
        innerFunction();
    }
    middleFunction();
}
outerFunction();

在这个例子中,innerFunction 访问变量时沿着作用域链依次查找,从自身作用域开始,然后到 middleFunction 的作用域,再到 outerFunction 的作用域,最后到全局作用域,从而能够正确访问到 globalValouterValmiddleVal

作用域链的形成与函数的定义和调用密切相关。当一个函数被定义时,它就创建了一个作用域,这个作用域包含了函数内部定义的变量以及它可以访问到的外部作用域变量。当函数被调用时,会创建一个新的执行上下文,这个执行上下文会包含一个作用域链。例如:

function createFunction() {
    var localVar = '局部变量';
    return function() {
        console.log(localVar); 
    };
}
var newFunc = createFunction();
newFunc();

createFunction 函数内部返回了一个匿名函数。这个匿名函数在定义时,它的作用域链包含了 createFunction 函数的作用域(因为它可以访问 createFunction 中的 localVar 变量)。当 createFunction 函数执行完毕后,localVar 变量本应随着 createFunction 函数的执行上下文被销毁,但由于匿名函数引用了 localVarlocalVar 变量依然存在于内存中,通过匿名函数依然可以访问到它。这就是闭包的原理,而作用域链是实现闭包的关键因素之一。

JavaScript中的变量生命周期

变量的生命周期指的是变量从创建到被销毁的整个过程。在JavaScript中,变量的生命周期与作用域密切相关。

全局变量的生命周期

全局变量在脚本开始执行时创建,直到脚本执行结束才会被销毁。例如:

var globalLifeVar = '全局生命周期变量';
// 脚本执行过程中都可以访问globalLifeVar
// 脚本执行结束后,globalLifeVar才会被销毁

在浏览器环境中,如果全局变量没有被显式地设置为 null 或通过其他方式解除引用,即使页面卸载,全局变量可能依然存在于内存中,直到页面完全关闭。这可能会导致内存泄漏,尤其是在频繁加载和卸载页面的场景中。

函数作用域变量的生命周期

函数作用域变量在函数被调用时创建,函数执行结束时被销毁。例如:

function funcLife() {
    var funcLifeVar = '函数生命周期变量';
    // 函数执行过程中可以访问funcLifeVar
}
funcLife();
// 函数执行结束后,funcLifeVar被销毁,这里无法访问

funcLife 函数被调用时,funcLifeVar 变量被创建并分配内存空间。函数执行完毕后,funcLifeVar 所在的函数执行上下文被销毁,funcLifeVar 也随之被销毁,其所占用的内存空间被回收。

块级作用域变量的生命周期

使用 letconst 定义的块级作用域变量,在进入块时创建,离开块时被销毁。例如:

{
    let blockLifeLet = '块级作用域let变量';
    const blockLifeConst = '块级作用域const变量';
    // 在块内可以访问blockLifeLet和blockLifeConst
}
// 离开块后,blockLifeLet和blockLifeConst被销毁,这里无法访问

在上述代码中,当进入花括号所定义的块时,blockLifeLetblockLifeConst 变量被创建。当离开该块时,它们所在的块级作用域执行上下文被销毁,这两个变量也随之被销毁。

需要注意的是,let 定义的变量存在“暂时性死区”的概念。例如:

console.log(blockLet); 
let blockLet = '块级作用域let变量';

在上述代码中,在 let blockLet 声明之前访问 blockLet 会抛出 ReferenceError,这是因为从块的开始到 let blockLet 声明之间的区域就是“暂时性死区”,在这个区域内 blockLet 虽然已经存在于作用域中,但不能被访问。而 var 定义的变量不存在“暂时性死区”,它会进行变量提升,例如:

console.log(varVar); 
var varVar = '函数作用域var变量';

这里虽然在 varVar 声明之前访问它,但不会抛出 ReferenceError,而是返回 undefined。这是因为 var 变量会被提升到函数或全局作用域的顶部,相当于:

var varVar;
console.log(varVar); 
varVar = '函数作用域var变量';

闭包与作用域和变量生命周期的关系

闭包是JavaScript中一个强大而又复杂的概念,它与作用域和变量生命周期紧密相连。简单来说,闭包是指函数可以记住并访问其所在的词法作用域,即使函数在该作用域之外被调用。

例如:

function outerClosure() {
    var outerClosureVar = '外部闭包变量';
    function innerClosure() {
        console.log(outerClosureVar); 
    }
    return innerClosure;
}
var closureFunc = outerClosure();
closureFunc();

在上述代码中,outerClosure 函数返回了 innerClosure 函数。innerClosure 函数在定义时,它的作用域链包含了 outerClosure 函数的作用域,所以它可以访问 outerClosureVar 变量。当 outerClosure 函数执行完毕后,正常情况下 outerClosureVar 变量应该随着 outerClosure 函数的执行上下文被销毁。但由于 innerClosure 函数形成了闭包,对 outerClosureVar 变量的引用依然存在,所以 outerClosureVar 变量不会被销毁,依然可以通过 closureFunc(即 innerClosure 函数的实例)访问到它。

闭包的存在使得变量的生命周期得到了延长。原本在函数执行结束后应该被销毁的变量,因为闭包的引用而继续存在于内存中。这在一些场景下非常有用,比如实现模块模式:

var counterModule = (function() {
    var count = 0;
    return {
        increment: function() {
            count++;
            console.log(count); 
        },
        decrement: function() {
            if (count > 0) {
                count--;
                console.log(count); 
            }
        }
    };
})();
counterModule.increment(); 
counterModule.decrement(); 

在这个模块模式的例子中,count 变量定义在立即执行函数内部,通过闭包,incrementdecrement 函数可以访问和修改 count 变量。count 变量不会因为立即执行函数执行完毕而被销毁,从而实现了对 count 变量的封装和持久化。

然而,闭包也可能会导致内存泄漏问题。如果闭包引用的变量占用大量内存,并且没有及时解除引用,就会造成内存浪费。例如:

function memoryLeakClosure() {
    var largeArray = new Array(1000000).fill(1); 
    return function() {
        console.log(largeArray.length); 
    };
}
var leakFunc = memoryLeakClosure();
// 如果leakFunc一直存在且未被释放,largeArray也会一直存在于内存中,导致内存泄漏

在这个例子中,largeArray 是一个占用大量内存的数组。由于返回的闭包函数引用了 largeArray,如果 leakFunc 一直存在且未被释放,largeArray 也会一直存在于内存中,从而可能导致内存泄漏。

作用域和变量生命周期对性能的影响

理解作用域和变量生命周期对于编写高效的JavaScript代码至关重要,它们对性能有着多方面的影响。

作用域链查找性能

当访问一个变量时,JavaScript引擎需要沿着作用域链进行查找。作用域链越长,查找变量所花费的时间就越长。例如:

var globalScopeVar = '全局作用域变量';
function outerPerformance() {
    var outerPerformanceVar = '外部函数作用域变量';
    function innerPerformance() {
        var innerPerformanceVar = '内部函数作用域变量';
        console.log(globalScopeVar); 
        console.log(outerPerformanceVar); 
        console.log(innerPerformanceVar); 
    }
    innerPerformance();
}
outerPerformance();

innerPerformance 函数中访问 globalScopeVar 时,引擎需要从 innerPerformance 作用域开始,依次向上查找,直到全局作用域才能找到该变量。相比之下,如果在 innerPerformance 函数内部定义一个同名变量,查找速度会更快,因为在当前作用域就能找到。

为了提高性能,应尽量减少作用域链的长度。可以通过合理组织代码,将相关的变量和函数放在合适的作用域内。例如,避免在深层嵌套的函数中频繁访问全局变量,可以将全局变量传递给内层函数作为参数。

变量生命周期与内存管理

变量的生命周期直接关系到内存的使用和管理。如果变量的生命周期过长,可能会占用不必要的内存,导致内存泄漏。例如,全局变量在整个脚本执行过程中都存在,过度使用全局变量可能会造成内存浪费。

在函数作用域中,如果函数内部定义的变量在函数执行完毕后不再被使用,应及时解除对这些变量的引用,以便垃圾回收机制能够回收它们所占用的内存。例如:

function memoryRelease() {
    var largeObject = {
        data: new Array(1000000).fill(1) 
    };
    // 对largeObject进行一些操作
    largeObject = null; 
}
memoryRelease();

在上述代码中,当对 largeObject 的操作完成后,将其设置为 null,解除了对这个占用大量内存的对象的引用,这样垃圾回收机制就可以回收 largeObject 所占用的内存。

对于闭包导致的内存泄漏问题,要谨慎使用闭包,确保在不需要闭包时及时解除对闭包函数的引用。例如:

function closureMemory() {
    var largeData = new Array(1000000).fill(1); 
    return function() {
        console.log(largeData.length); 
    };
}
var closureFunc = closureMemory();
// 当不再需要closureFunc时
closureFunc = null; 

通过将 closureFunc 设置为 null,解除了对闭包函数的引用,从而使得 largeData 有可能被垃圾回收机制回收,避免了内存泄漏。

总结

JavaScript中的作用域和变量生命周期是深入理解这门语言的关键概念。作用域决定了变量的可见性和可访问区域,包括全局作用域、函数作用域和ES6引入的块级作用域。作用域链则定义了变量查找的规则,从内向外依次查找。变量的生命周期与作用域紧密相关,全局变量生命周期贯穿脚本执行全程,函数作用域变量在函数调用时创建、结束时销毁,块级作用域变量在进入块时创建、离开块时销毁。

闭包作为JavaScript的一个重要特性,依赖于作用域和变量生命周期,它可以延长变量的生命周期,但也可能导致内存泄漏。理解作用域和变量生命周期对性能的影响,如作用域链查找性能和内存管理,对于编写高效、健壮的JavaScript代码至关重要。通过合理利用作用域和管理变量生命周期,可以避免命名冲突、内存泄漏等问题,提高代码的质量和性能。