前言
闭包这个问题快被问烂了
这篇文章会从函数执行的过程详细说一下闭包
希望对大家有所帮助
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎点赞。
什么是闭包?
借用MDN的官网上说 : 能够访问自由变量的函数就是闭包
什么是自由变量?
还是MDN官网:自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
有的同学有疑问了
var a = "小伙子真帅"
function foo(){
console.log(a)
}
按照MDN上解释
- 首先foo是函数
- 然后访问了变量a
- 变量a既不是函数也不是函数的局部变量的变量
- 这是个闭包?
哎!你他娘的还真是天才
某些角度上来讲一切js函数都是闭包。
这不乱了套了,没关系为防止这种情况 ECMAScript 中 把闭包分成了两个角度
- 从理论角度:切js函数都是闭包。
- 从实际角度:只有以下的函数才能称之为闭包
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回,一个函数访问了外部函数作用域中的变量) 比较惭愧之前面试官问这道题的答案基本说的都是这一步
- 在代码中引用了自由变量
由此我们得出了一个公式
闭包 = 函数 + 函数能够访问的自由变量
现在我们思考这样的一个问题:
函数为什么能够访问的自由变量?
解决这个问题你会对闭包有深刻的理解
接下来我们需要逐步分析一下执行上下文的过程,从实际角度上来解释闭包
看一段代码
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
它的过程是:
- 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
ECStack = [
globalContext
];
- 全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
- 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
- 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
}
- checkscope 函数执行
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: "local scope",
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
- 执行完毕checkscopeContext 弹出
ECStack = [
globalContext
];
- 执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
ECStack = [
fContext,
globalContext
];
-
函数执行上下文初始化,跟上个是一样的不同的是他依旧会复制父级函数的 [[scope]] 属性创建作用域链
这里说一下作用域,他在函数写下来的时候就已经确定了,不会因为函数执行完毕而消失
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, globalContext.VO],
this: undefined
}
- f函数执行
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, globalContext.VO],
this: undefined
}
- 执行完毕 弹出
ECStack = [
globalContext
];
我们看步骤8我们知道了f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
就是这里解释了函数为什么能够访问自由变量。
即便checkscopeContext 被销毁了,但是 js 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 js 做到了这一点,从而实现了闭包这个概念。
而闭包导致内存泄漏的根本原因也在这里。
就是因为 checkscopeContext.AO 活在内存中不能被垃圾机制回收,要想避免内存泄漏我们需要将 foo = null
总结
- 从理论角度:一切js函数都是闭包。
- 从实际角度:只有以下的函数才能称之为闭包
- 即使创建它的上下文已经销毁,它仍然存在
- 在代码中引用了自由变量
由此看闭包不过如此
参考文献 : github.com/mqyqingfeng…