作用域链和闭包

55 阅读5分钟
function foo() {
    console.log(myName);
}

function bar() {
    var myName = '小竹合';
    foo();
}

var myName = '河图';
bar();

思考这个会打印出来什么内容?

执行foo()函数内部时,这一段代码的调用栈如下

image.png

一、作用域链

每个执行上下文的变量环境中都包含了一个外部引用,用来指向外部的执行上下文,这个外部引用称为outer

当一段代码使用了一个变量时,JS引擎会在当前的变量环境中查找,如果没有查找到,JS会在outer所指向的执行上下文中查找。

如下:

image.png 这个查找的链条称为作用域链

二、词法作用域

词法作用域是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

例如:

1、

let a = 1;
function foo() {
    let a = 2;
    function bar() {
        let a = 3;
    }
}

这段代码的词法作用域及词法作用域链如下

image.png 2、

let a = 1;
function foo() {
    let a = 2;
}
function bar() {
    let a = 3;
}

这段代码的词法作用域及词法作用域链如下

image.png

词法作用域是代码阶段就决定好的,和函数怎么调用没有关系当访问一个变量时,JavaScript引擎会沿着词法作用域链向上查找,直到找到变量或达到全局作用域。

三、闭包

闭包(Closure)产生于函数的嵌套,其中内部函数引用了外部函数的变量。这样的内部函数可以“捕获”外部函数的变量,即使外部函数已经执行完毕。

一个闭包实际上是由一个函数和对该函数的引用的一个引用环境组合而成。当内部函数引用了外部函数的变量时,就形成了一个闭包。

function outerFunction() {
    var outerVar = 'I am an outer variable'

    function innerFunction() {
        console.log(outerVar)
    }

    return innerFunction
}

// 创建一个闭包
var closure = outerFunction()

// 调用闭包,它仍然能够访问 outerVar
closure() // 输出 "I am an outer variable"

执行closure函数内部return innerFunction这个代码的时候的调用栈

image.png 根据词法规则,内部函数innerFunction可以访问到外部函数outerFunction中的outerVar变量。

闭包的特性:

  • 访问外部变量: 内部函数可以访问外部函数中声明的变量,即使外部函数已经执行完毕。
  • 保持状态: 闭包可以用于保持局部变量的状态,使其在多次调用之间保持不变。

闭包的应用场景

  • 封装私有变量: 通过闭包,可以模拟实现私有变量,防止外部直接访问。
function counter() {
    var count = 0;

    return function () {
        return ++count;
    };
}

var cnt = counter();
console.log(cnt()); // 输出 1
console.log(cnt()); // 输出 2
  • 函数工厂: 通过闭包,可以创建一个生成特定类型函数的工厂。
function multiplier(factor) {
    return function (x) {
        return x * factor;
    };
}

var double = multiplier(2);
console.log(double(5)); // 输出 10
  • 异步操作: 闭包在处理异步操作时非常有用,可以保留异步操作所需的上下文信息。
function fetchData(url, callback) {
    // 模拟异步操作
    setTimeout(function () {
        var data = "zhuhe " + url;
        callback(data);
    }, 1000);
}

function processData(data) {
    console.log("=====> " + data); // =====> zhuhe 真帅
}

fetchData("真帅", processData);

使用闭包需要注意的地方

  • 内存管理: 当闭包存在时,外部函数的局部变量不会被垃圾回收,直到闭包不再被引用。
  • 性能考虑: 过度使用闭包可能导致内存泄漏和性能问题,因此需要谨慎使用。

闭包如何回收详细查看垃圾回收机制:垃圾数据是如何回收的这一篇文章,对JS的垃圾回收机制有详细的讲解。

内存泄漏

内存泄漏是指在程序中分配的内存由于某些原因没有被正确释放或回收,导致程序持续占用更多的内存空间,最终可能导致系统资源耗尽或程序性能下降。

以下是一些可能导致内存泄漏的常见原因:

  1. 未释放动态分配的内存: 如果程序分配了内存(例如使用 newmalloc),但没有正确释放它(使用 deletefree),就会导致内存泄漏。

JavaScript使用垃圾回收器自动管理内存,因此开发者不需要手动释放内存。

  1. 循环引用: 在支持自动垃圾回收的语言中,循环引用是一种常见的内存泄漏情况。如果对象之间存在相互引用,但这些对象已经超出了程序的可访问范围,垃圾回收器无法释放它们。

在现代的JavaScript引擎中,有强大的垃圾回收机制,可以处理循环引用的情况,确保不再被引用的对象会被垃圾回收器回收。

  1. 未关闭文件或网络连接: 在一些编程环境中,打开的文件或网络连接占用的资源需要显式地关闭。如果忘记关闭这些资源,就会导致内存泄漏。

JavaScript通常不涉及文件和网络连接的操作,这些资源的管理通常由底层平台或浏览器处理。

  1. 定时器和事件监听器: 在使用定时器或添加事件监听器时,如果未正确取消定时器或删除监听器,这些操作可能会引起内存泄漏,因为它们会保留对对象的引用。

在JavaScript中,定时器和事件监听器是常见的操作,但它们在不再需要时可以手动清理,而不会导致显式的内存泄漏。

  1. 全局变量: 在全局作用域中创建的变量不会在程序执行期间被释放,直到程序退出。如果不再需要的全局变量没有被及时清理,就会导致内存泄漏。