从函数执行的过程聊一下闭包

100 阅读4分钟

前言

闭包这个问题快被问烂了

这篇文章会从函数执行的过程详细说一下闭包

希望对大家有所帮助

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎点赞。

什么是闭包?

借用MDN的官网上说 : 能够访问自由变量的函数就是闭包

什么是自由变量?

还是MDN官网:自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

有的同学有疑问了

var a = "小伙子真帅"

function foo(){
    console.log(a)
}

按照MDN上解释

  1. 首先foo是函数
  2. 然后访问了变量a
  3. 变量a既不是函数也不是函数的局部变量的变量
  4. 这是个闭包?

哎!你他娘的还真是天才

某些角度上来讲一切js函数都是闭包。

这不乱了套了,没关系为防止这种情况 ECMAScript 中 把闭包分成了两个角度

  1. 从理论角度:切js函数都是闭包。
  2. 从实际角度:只有以下的函数才能称之为闭包
  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回,一个函数访问了外部函数作用域中的变量) 比较惭愧之前面试官问这道题的答案基本说的都是这一步
  • 在代码中引用了自由变量

由此我们得出了一个公式

闭包 = 函数 + 函数能够访问的自由变量

现在我们思考这样的一个问题:

函数为什么能够访问的自由变量?

解决这个问题你会对闭包有深刻的理解

接下来我们需要逐步分析一下执行上下文的过程,从实际角度上来解释闭包

看一段代码


var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

它的过程是:

  1. 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
    ECStack = [
        globalContext
    ];
  1. 全局上下文初始化

    globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

  1. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
    ECStack = [
        checkscopeContext,
        globalContext
    ];
  1. checkscope 函数执行上下文初始化
  • 复制函数 [[scope]] 属性创建作用域链,
  • 用 arguments 创建活动对象
  • 初始化活动对象,即加入形参、函数声明、变量声明
  • 将活动对象压入 checkscope 作用域链顶端。

同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
  1. checkscope 函数执行
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: "local scope",
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
  1. 执行完毕checkscopeContext 弹出
    ECStack = [
        globalContext
    ];
  1. 执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
    ECStack = [
        fContext,
        globalContext
    ];
  1. 函数执行上下文初始化,跟上个是一样的不同的是他依旧会复制父级函数的 [[scope]] 属性创建作用域链

    这里说一下作用域,他在函数写下来的时候就已经确定了,不会因为函数执行完毕而消失

    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
  1. f函数执行
    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
  1. 执行完毕 弹出
    ECStack = [
        globalContext
    ];

我们看步骤8我们知道了f 执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

就是这里解释了函数为什么能够访问自由变量。

即便checkscopeContext 被销毁了,但是 js 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 js 做到了这一点,从而实现了闭包这个概念。

而闭包导致内存泄漏的根本原因也在这里。

就是因为 checkscopeContext.AO 活在内存中不能被垃圾机制回收,要想避免内存泄漏我们需要将 foo = null

总结

  1. 从理论角度:一切js函数都是闭包。
  2. 从实际角度:只有以下的函数才能称之为闭包
  • 即使创建它的上下文已经销毁,它仍然存在
  • 在代码中引用了自由变量

由此看闭包不过如此

参考文献 : github.com/mqyqingfeng…