javascript之闭包

本文详细解释了JavaScript中闭包的概念,包括自由变量、执行上下文、作用域链等关键知识点,通过实例代码展示了闭包如何形成及可能导致的内存泄漏问题。

前言

闭包就是能够读取其他函数内部变量的函数。在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。首先理解什么是自由变量?

正文

什么是自由变量?

指在当前作用域中使用,却不属于当前作用域的变量

例:

    'use strict';
    
    function foo() {
        var a = 1;
        function bar() {
            console.log(a);
        }
        bar();
    }
复制代码

在foo函数中,a变量相对了bar函数就是自由变量。

闭包

首先需要知道几个概念:

执行上下文初始化的时候包含:

  • this
  • 活动对象(Active Object),在全局的时候即(Global Object)
  • 作用域链([[scope]]),存在函数执行上下文中

每个函数内部都有一个[[scope]]的属性(这个属性仅供js引擎使用),当函数被创建的时候,[[scope]]属性会保存创建该函数的作用域中的所有对象(Variable Obejct)。

具体通过下面代码的执行过程来看闭包:

    'use strict';
    
    function foo() {
        var a = 1;
        return function() {
            return ++a;
        };
    };
    
    var add = foo();
    console.log(add()); // 2
复制代码

该代码的具体执行过程如下:

  1. 创建全局执行上下文环境(Global Execution context,下面简称globalEC ),将全局执行上下文压入执行上下文栈(Execution context stack,下面简称ECStack)中。
    // 伪代码,全局执行上下文压栈
    ECStack = [
        globalEC
    ]
复制代码
  1. 进入预解析阶段,变量提升和函数提升
    function foo() { /... };
    var add;
    
    add = foo();
    console.log(add());
复制代码
  1. 全局执行上下文初始化
    // 伪代码 
    globalEC = {
        [global]: [global],
        this: [global],
        GO: {
            foo: function() { //... },
            add: undefined
        }
    }
复制代码
  1. 当执行foo函数的时候,创建foo函数的执行上下文环境(foo Execution context,下面简称fooEC),并压入执行上下文栈中
    // 伪代码,创建foo函数的执行上下文并压栈
    ECStack = [
        fooEC
    ]
    
复制代码
  1. foo函数执行上下文初始化,并处于活动状态
    // 伪代码
    fooEC = {
        this: undefined, // 严格模式下为undefined,非严格模式下为window
        AO: {
            arguments: {
                length: 0
            },
            a: undefined
        },
        [[scope]]: [fooEC.AO, globalEC.GO]
    }
复制代码
  1. foo函数开始执行
    // 伪代码,对变量a赋值
    fooEC = {
        this: undefined,
        AO: {
            arguments: {
                length: 0
            },
            a: 1
        },
        [[scope]]: [fooEC.AO, globalEC.GO]
    }
复制代码
  1. foo函数执行完毕,foo函数的执行上下文出栈,foo函数的执行上下文销毁,但此时foo函数中的变量a(对于add函数来说,a就是自由变量)还被add函数引用,此时就形成了闭包,add函数的作用域链中保存了foo函数中的活动对象,所以导致foo函数的活动对象不能被销毁。
    // 伪代码,foo函数的执行上下文出栈,进入全局执行上下文环境
    ECStack = [
        globalEC
    ]
复制代码
  1. 当执行到add函数的时候,创建add函数的执行上下文环境(add Execution context,下面简称addEC),并压栈
    //伪代码,add函数的执行上下文压栈
    ECStack = [
        addEC,
        globalEC
    ]
复制代码
  1. add函数执行上下文初始化,并处于活动状态
    // 伪代码,add函数执行上下文初始化
    addEC = {
        this: undefined,
        AO: {
            aruments: {
                length: 0
            }
        },
        [[scope]]: [[addEC.AO, fooEC.AO, globalEC.GO]]
    }
复制代码

可以看出,此时add函数的作用域链中还保存着foo函数的活动对象。

  1. add函数执行完毕,add函数的执行上下文出栈,并被销毁,进入全局执行上下文环境
    // 伪代码
    ECStack = [
        globalEC
    ]
复制代码

至此,整个执行过程就结束了,但是由于函数的内部的[[scope]]属性是在函数被创建的时候就存在的,里面保存了定义该函数时候的作用域链,在上面的示例代码中,即:add函数中保存了foo函数的作用域链。所以导致foo函数作用域链中的活动对象还存在引用,不能得到释放,占用了内存,这也就是为什么闭包会导致内存溢出的原因。

所以当不再需要使用add函数的时候,将add函数的赋值为null,即解除了add函数的引用,也就释放add函数占用的内存。

代码如下:

    'use strict';
    
    function foo() {
        var a = 1;
        return function() {
            return ++a;
        };
    };
    
    var add = foo();
    console.log(add()); // 2
    
    //如果后面不再使用add函数,即将其赋值为null来释放内存
    add = null;
复制代码

面试常考题

示例代码:

    'use strict';
    
    for(var i=0; i<3; i++) {
        setTimeout(function() {
            console.log(i);
        });
    }
复制代码

结果:上面的代码会在控制台打印3,打印3次。

解析:

  1. 进入全局上下文环境,压栈,声明变量a
  2. 当每次进入for循环的时候,会将 console.log(i) 放入异步队列中
  3. 当主线程的代码执行完成,就会去执行异步队列中的代码
  4. 此时for循环已经执行完成,此时i的值为3
  5. 因为setTimeout函数此时处于全局执行上下文中,所以当要查找变量 i 的时候,会去全局对象中找,i的值已经变为3。

解决方案: 只要setTimeout函数处于块作用域中,就能保存每次循环i的值。

  1. 闭包
    'use strict';
    
    for(var i=0; i<3; i++) {
        (function(i) {
            setTimeout(function() {
                console.log(i);
            });
        })(i);
    }
复制代码

此时,每次for循环的时候,将i的值传递进来,保存到自执行函数的arguments里面,这样就形成了一个函数作用域,那么每次寻找变量i的时候,就先从当先的作用域中查找。

  1. 可以使用let声明,使用let可以产生一个块作用域,也能保存 i 的值
    'use strict';

    for(let i=0; i<3; i++) {
        setTimeout(function() {
            console.log(i);
        });
    }
复制代码

当然还可以有别的方法实现,但重要的是搞清楚原理。

结语: 上面的过程是我看了多篇关于闭包的文章之后自己的理解,如有不对的地方,还请指正~~~///(^v^)\~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值