JavaScript闭包
在我们说闭包时我们先来看看两个东西:作用域和生存周期
1.作用域
看一个例子
function fn1(){
let str = "我是一个帅哥";
};
function fn2(){
//我们尝试使用这个变量
console.log(str);//Uncaught ReferenceError: str is not defined
};
fn1();
fn2();
因为str是一个局部变量就像函数的私有财产局部变量只能在定义它的函数内使用。
我们再看下面例子
function fn1() {
let str = "我是一个帅哥";
function fn2() {
//我们尝试使用这个变量
alert(str);//正常弹出窗口
}
fn2();
}
fn1();
原因非常简单,因为在JS中,子函数可以直接使用父函数的局部变量
那我们再看一段代码
let str = "我是一个帅哥";
function fn1() {
function fn2() {
//我们尝试使用这个变量
alert(str);//还是能正常弹出窗口
}
fn2();
}
fn1();
在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。
2.生存周期
说起生存周期,我们需要先从“垃圾回收”这个概念说起,我们都知道,计算机的资源是有限的,所以用完的东西就要尽快释放掉,而垃圾回收粗略的可以分两个阶段:
1.手动回收:C/C++为代表
2.自动回收:Java、JS、Python等高级语言为代表,自动回收内存,方便又安全,妈妈再也不用担心我内存泄漏的问题
GC(自动垃圾收集机制)是非常方便的,但垃圾回收需要一定的标准,不能把别人还在用的东西回收了
3.作用域决定了谁是垃圾
function show(){
let a = '我是帅哥'
}
//简易分为三个阶段
//1.show执行前:a是不存在的,不占空间
show(); //2.show执行时:a被分配空间
//3.show结束后:a的生存周期结束,可以被回收
所以,局部变量(通常)在函数执行后,就会被回收(当然,变量其实并没有被立即回收,而是被标记为“可回收”,在下一次GC工作时被带走)。
我们再回到闭包,如果上面明白了我们再看下面的例子
function show(){
let str = '你们也是帅哥';
document.onclick = function(){
alert(str);
};
};
//执行前,str不存在
show(); //执行时,str被创建
//执行后,str“本应”被回收
在上面的例子中,我们show中的局部变量str,本应在函数结束后被回收(局部变量在函数结束后被回收),但是实际情况是,不论你10分钟还是1天以后点击页面,这个str都还在,并没有被回收,原因很简单
函数的存在,延长了外层局部变量的生存周期,只要这个onclick函数还在,那么它onclick函数外面的局部变量就不会回收
1.js如何确定哪些父级变量要保留?
结论是,闭包会保留全部父级变量,不论用没用,这是考虑到:健壮性:代码是一个极其复杂的东西,理论上官方不可能100%不出错的确定哪些用了哪些没用,而如果误回收了变量,会导致程序崩溃,所以全都保留是出于程序健壮性的考虑。
性能考虑:就算假设有办法判定一段代码里到底用了什么,也会是非常复杂的,会造成大量的运行时开销,反而造成程序性能降低,得不偿失
2.js保留变量引用了计数机制。
那js如何知道在onclick失效时去回收哪些变量呢?这就要说到引用计数机制,简单看个例子:
let a = { name: "blue", age: 18 }; //{json}被a引用,所以json的引用计数加1
//引用计数=1
let b = a; //{json}又被b引用,引用计数为2
//引用计数=2
a = null; //a不再引用{json},引用计数减1,还剩1个
//引用计数=1
//此时依然有b在引用{json},所以并不回收
b = null; //引用计数又减1
//引用计数=0
//至此,{json}被标记为“可回收”
所谓引用计数,就是标明一个东西,在被几个人引用,一旦引用计数归零,则被标记为“可回收”而上面的onclick也是一样的道理
function show() {
let a = 12; //引用计数:1
document.onclick = function() {}; //引用计数:2
}
show(); //show运行中,引用计数:2
//函数结束,let a相关的引用消失,但并不为零,因为onclick还在引用自己的父级变量
//引用计数:1
//直到有一天...
document.onclick = null; //function没了,所以引用计数减1
//引用计数:0
//a标记为“可回收”
所以,js依靠引用计数机制,确定一个东西是否能回收
4.父级的父级
function fn1() {
let a = 12; //保留
function fn2() {
let b = 5; //保留
document.onclick = function() {
//a和b在onclick中都有可能被用到,所以,都延长
};
}
fn2();
}
fn1();
5.什么是闭包
把我们上面说的所有东西结合起来,闭包的结论就出来了
闭包是函数和周围(词法环境)的组合,换句话说,闭包可以使你访问外层的函数的作用域,在js中,每当你创建一个函数,闭包也会被创建。
- 闭包是js自身语法的一部分,只要你访问了父函数的局部变量,都算是“用到了”闭包,这是一个自然的过程,无需刻意使用
- 闭包保证了js自身的运行,如果错误的将外部变量回收掉,会导致js崩溃,所以闭包更多的是为js语言本身服务,而不是我们使用者