什么是闭包
闭包是指有权访问另外一个函数作用域中的变量的函数
关键点:
- 是一个函数
- 能访问另一个函数作用域中的变量(及时另一个函数的执行上下文已经销毁)
闭包的特性
- 闭包可以访问当前函数以外的变量
- 闭包可以更新闭包中外部变量的值
作用域链
作用域链是闭包实现的核心,js代码运行时,会创建若干个执行上下文,而函数会创建函数的执行上下文,在函数执行上下文中主要有两个组成部分,一个是环境记录,一个是对外部环境的引用
环境记录:
- 声明性环境记录(函数中的变量,函数以及函数的参数)
- 对象环境记录(用于定义在全局执行上下文中出现的变量和函数的关联。全局环境包含对象环境记录。)
对外部环境的引用,我们就称之为「作用域链」
当访问一个变量时,解释器会首先在当前的作用域查找对应的标识符,如果没有找到,就去父级作用域找,直到找到该变量的标识符或者不在任何一级作用域中
作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError。
作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中
举例说明
let a = 'global';
function myscope(){
let a = 'scope';
function f(){
return a;
}
return f;
}
let foo = myscope(); //foo指向函数f
foo(); // 调用函数f
执行过程如下:
- 进入全局代码,创建全局执行上下文,全局执行上下文压入执行栈
- 全局上下文初始化(函数变量声明,函数提升,变量提升等)
- 执行myscope函数,创建myscope执行上下文,myscope执行上下文压入执行栈
- myscope执行上下文初始化,创建变量对象,作用域链,绑定this
- myscope函数执行完毕,myscope执行上下文从执行栈中弹出
- 执行函数f,创建f执行上下文,f执行上下文压入执行栈
- f进行上下文初始化
- f函数执行完毕,f函数从执行栈中弹出
函数f执行的时候,myscope函数执行上下文已经从执行栈中弹出了,那么是如何访问到myscope函数中的变量的呢,关键就在于作用域链
fContext = {
Scop:[AO, myscopeContext.AO, globalContext.AO]
}
所以指向关系是当前作用域 --> myscope作用域--> 全局作用域,即使 myscopeContext.AO 被销毁了,但是 JavaScript 依然会让 myscopeContext.AO(活动对象) 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,这就是闭包实现的关键
闭包举例2
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
输出全部都是3
循环结束后,全局执行上下文的VO是
globalContext = {
VO: {
data: [...],
i: 3
}
}
执行 data[0] 函数的时候,data[0] 函数的作用域链为: data[0]Context = { Scope: [AO, globalContext.VO] }
改进方法
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i)
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
此时执行 data[0] 函数的时候,data[0] 函数的作用域链为: data[0]Context = { Scope: [AO, 匿名函数Context.AO, globalContext.VO] }
此时匿名函数的执行上下文AO为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
因为闭包执行上下文中贮存了变量i,所以根据作用域链会在globalContext.VO中查找到变量i,并输出0。
块级作用域(let)
同样是上面的例子,我们可以通过let来实现目的
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
let创建的块级作用域类似如下
var data = [];// 创建一个数组data;
// 进入第一次循环
{
let i = 0; // 注意:因为使用let使得for循环为块级作用域
// 此次 let i = 0 在这个块级作用域中,而不是在全局环境中
data[0] = function() {
console.log(i);
};
}
上面的块级作用域,就像函数作用域一样,函数执行完毕,其中的变量会被销毁,但是因为这个代码块中存在一个闭包,闭包的作用域链中引用着块级作用域,所以在闭包被调用之前,这个块级作用域内部的变量不会被销毁。
闭包的存在于销毁
闭包不是所有情况下都是持久存在的,如果存在对于闭包的引用,那么只要这个引用存在,那么闭包就会存在
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
// 执行完后,闭包是否会被垃圾回收
checkscope()(); // Yes
var foo = checkscope();
foo(); // No
checkscope()执行完成后,checkscope()()中自由变量特定时间之后回收,foo()中自由变量不回收。
现在主流浏览器的垃圾回收算法是标记清除,标记清除并非是标记执行栈的进出,而是从根开始遍历,也是一个找引用关系的过程,但是因为从根开始,相互引用的情况不会被计入。所以当垃圾回收开始时,从Root(全局对象)开始寻找这个对象的引用是否可达,如果引用链断裂,那么这个对象就会回收。
闭包中的作用域链中 parentContext.vo 是对象,被放在堆中,栈中的变量会随着执行环境进出而销毁,堆中需要垃圾回收,闭包内的自由变量会被分配到堆上,所以当外部方法执行完毕后,对其的引用并没有丢。
每次进入函数执行时,会重新创建可执行环境和活动对象,但函数的[[Scope]]是函数定义时就已经定义好的(词法作用域规则),不可更改。
对于第一种调用
checkscope()执行时,将checkscope对象指针压入栈中,其执行环境变量如下
checkscopeContext:{
AO:{
arguments:...
scope:...
f:...
},
this,
[[Scope]]:[AO, globalContext.VO]
}
执行完毕后出栈,该对象没有绑定给谁,从Root开始查找无法可达,此活动对象一段时间后会被回收
对于第二种执行方式,foo作为闭包的引用,执行的时候执行上下文为:
fContext:{
AO:{
arguments:
},
this,
[[Scope]]:[AO, checkscopeContext.AO, globalContext.VO]
}
此对象赋值给var foo = checkscope();,将foo压入栈中,foo指向堆中的f活动对象,对于Root来说可达,不会被回收。
如果需要scope的回收,取消引用即可,将foo = null