一、概念
1、域
在js中,作用域分为全局作用域与局部作用域
- 全局作用域:由<script>标签产生的区域,从计算机的角度可以理解为window对象
- 局部作用域:有函数产生的区域,从计算机的角度可以理解为该函数的AO对象
注意:在ES6之前,只有函数可以产生作用域,是没有块级作用域这一说的
2、作用域链
JavaScript高级程序设计中解释:
关于活动对象AO和GO对象的讲解请看上一篇:点我查看
在js中,函数存在一个内部隐式属性[[scopes]],这个属性用来保存当前函数在执行时的环境(上下文),由于在数据结构上是链式的,也被称为作用域链,我们可以把它理解为一个数组。
function a() {}
console.dir(a); // 打印内部结构
打印结果:
[[scopes]]属性在函数声明时产生,在函数调用时更新
[[scopes]]属性记录当前函数的执行环境
示例:
function a() {
console.dir(a);
function b() {
console.dir(b);
function c() {
console.dir(c);
}
c();
}
b();
}
a();
注意:控制台打印中的[[scopes]]是函数声明时的执行环境。
要看函数运行时的执行环境,要打断点看一下:
关于AO对象和GO对象的讲解请看上一篇:点我查看
(1)最开始只有GO:
(2)往下执行,函数a被执行,产生了a的AO对象
此时a的scopes:
- 0:a的AO对象,里面有b,是个函数
- 1:GO
(3)往下执行,函数b被执行,产生了b的AO对象
此时b的scopes:
- 0:b的AO对象,里面有c,是个函数
- 1:a的AO对象,里面有b,是个函数
- 2:GO
(4)往下执行,函数c被执行,产生了c的AO对象
此时c的scopes:
- 0:c的AO对象
- 1:b的AO对象,里面有c,是个函数
- 2:a的AO对象,里面有b,是个函数
- 3:GO
二、作用域链的作用
在访问变量或者函数时,会在作用域链上依次查找,最直观的表现是:
- 内部函数可以访问外部函数声明的变量
例子:
function a() {
var aa = 111;
function b() {
var aa = 222;
console.log(aa); // 222
}
b();
}
a();
分析:
1、产生a的AO对象,aAO
- aAO:{aa: undefined, b: fun}
- 执行函数a,aAO变为:{aa: 111, b: fun}
2、产生b的AO对象,bAO
-
bAO:{aa: undefined}
-
执行函数b,bAO变为{aa: 222}
-
[[scopes]] :
0:bAO
1:aAo
2:Go
结果:所以先去bAO里面找aa,打印222
三、闭包
- 如果在内部函数使用了外部函数的变量,就会形成闭包,闭包保留了外部环境的引用。
- 如果内部函数被返回到了外部函数的外面,在外部函数执行以后,依然可以使用闭包中的值。
- 在内部函数b中使用了外部函数a中的变量,这个变量就会作为闭包对象中的属性
1、闭包的形成
产生闭包的条件
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/数据)
例子1:形成闭包
function a() { // 外部函数
var aa = 100;
function b() { // 内部函数
console.log(aa);
}
b();
} // 到这里时, b 函数执行完毕,闭包消失
a()
我们可以打断点看一下:
要打印变量aa的时候:
函数b执行完以后,闭包被销毁
说明,形成了闭包,但是闭包并没有保持。
例子2:形成闭包
function a() {
var aa = 100;
function b() {
console.log(b);
}
b();
}
a()
例子3:未形成闭包
function a() {
var aa = 100;
function b() {
var b = 10;
console.log(b);
}
b();
}
a()
2、闭包的保持
如果希望在函数调用后闭包依然保持,就需要将内部函数返回到外部函数的外部
function a() {
var num = 0;
function b() {
console.log(num++);
}
return b;
}
var demo = a();
console.dir(demo);
demo();
demo();
打印结果:
过程分析:
- GO:{demo: undefined,a:fun}
- 调用函数a,产生aAO
- aAO:{num:undefined,b:fun}
- 预编译结束,开始执行代码
- 执行第二行代码,num值为0,aAO:{num:0,b:fun},执行时3-5行代码会被略过(看不懂的看我上一篇),把b返回出去
- 此时demo里面保存的是b,GO:{demo:b,a:fun}。但是这个时候b还未被执行
- 执行第10行代码,demo(),相当于执行函数b,产生bAO
- bAO:{}
- b的作用域 [[scopes]]:
0: bAO
1: aAO
2: GO - 执行b ,打印num,bAO中没有num,去找aAO,aAO中有num,形成闭包,打印出0,num值加1,aAO变为,aAO:{num:1,b:fun}
- 第10行执行完毕,bAO会被销毁
- 第11行代码执行,产生一个新的bAO对象,过程类似上述7-11行。
3、总结
函数执行分成两个阶段(预编译阶段和执行阶段):
- 在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
- 执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
3.1 使用闭包要满足两个条件
- 闭包要形成,在内部函数使用外部函数的变量
- 闭包要保持,内部函数返回到外部函数的外面
3.2 闭包是什么
理解一:闭包是嵌套的内部函数(绝大部分人)
理解二:包含被引用变量(函数)的对象(少数人)
注意:闭包存在于嵌套的内部函数中。
4、闭包的优缺点
优点:
① 跨作用域访问变量,让函数外部也可以访问到函数内的变量。
② 让这些变量一直存在于内存中,不会在调用结束后,被垃圾回收机制回收
③ 避免全局变量的污染
缺点:
① 函数执行完后,函数的局部变量没有释放,占用内存时间变长
② 容易造成内存泄漏(白白占用着,别人又用不上)
缺点解决:
① 能不用闭包就不用
② 及时释放
及时释放:
function fn(){
var arr = new Array(100);
function fn1(){
console.log(arr.length);
}
return fn1;
}
var f = fn();
f();
f = null; //让内部函数成为垃圾对象-->回收闭包