# 词法环境[[environment]]--闭包
# 闭包定义
一些书籍中对于闭包的定义:
- 闭包是指有权访问另一个函数作用域中的变量的函数---《JavaScript高级程序设计》
- 闭包允许函数访问并操作函数外部的变量---《JavaScript忍者秘籍》
- 闭包使得函数可以继续访问定义时的词法作用域---《你不知道的JavaScript》
- 所有JavaScript函数都是闭包---《JavaScript权威指南》
MDN:
- 函数和对其周围状态(词法环境)的引用捆绑在一起构成闭包。
- 也就是说,闭包可以让你从内部函数访问外部函数作用域(作用域链)。
- 在JavaScript中,每当函数被创建,就会在函数生成时生成闭包。
# [[Environment]]
[[Environment]]
:内见属性
- 每个函数都有;
- 存放词法环境的作用;
- v8引擎中无法读写;
对比之前学过的另一个内见属性[[Prototype]]
- 每个函数(构造函数)都有;
- 存放对象原型的作用;
- 可以使用
__proto__
读写;
[[Environment]]
叫词法环境对象:
- 整个脚本文件执行前会产生一个
- 函数实例创建后会产生一个
[[Environment]]
属性记录了:当前函数的词法环境对象==>外层函数的词法环境对象==>全局的词法环境对象;这样就形成了作用域链。
举例说明:
function fn() {
const a = 1;
function fo() {
console.log('hi')
}
}
fn()
//此时fn函数的[[Environment]]属性包括:作用域链
//1.当前环境的记录
a:1
fo:()=>console.log('hi')
//2.对外部词法环境的引用(全局词法环境)
闭包的例子:函数与词法环境捆绑
function fn() {
let a = 1;
return function fo() {
console.log(a++)
}
}
let x = fn()//fn()执行后的返回值fo被引用到了全局变量x中
x();//1
x();//2
//下面两种情况相当于创建了两个实例,
//当前面的函数执行完之后里面的变量和函数就被垃圾回收掉,
//所以不影响后面函数的执行依然打印的是a=1
fn()()//1
fn()()//1
我们分析一下上面函数不考虑词法环境的情况下的执行调用栈:
fn()
入栈;变量a入栈;fn()
和a一起出栈;x()
入栈;(此时出现问题了,x()
在执行过程中需要找到a,但是a又没在执行栈中,并不符合我们要的预期)
此时就需要在函数与词法环境捆绑,形成闭包来实现这个问题:函数执行前就形成词法环境写入[[Environment]]
属性中:此时fo函数内部词法环境是只有一个console.log函数,外层函数词法环境是a:1
,全局词法环境为空。
函数中在执行过程中的内部变量,并不是同函数一样被加入到调用栈中,而是在函数执行前形成词法环境写入[[Environment]]
属性中保存。只要有函数就会有闭包,只是v8中有垃圾回收机制,当函数A执行完时,返回另一个函数B,函数B又使用全局变量来保存,且函数B中引用了函数A中的变量,那么函数A中被引用的变量就永远不会被回收掉,需要我们手动进行变量回收。(需要注意的是:在js中定义的全局变量是不会被销毁的,因为随时都可能会用到这个变量,所以不能被销毁)
function f() {
function f1() {
var n = 1;
function f2() {
return console.log(n = n + 1)
};
return f2;
}
let x = f1()
x();
x();
}
f();
//如果是这样的话,函数f在执行完毕之后,变量n依然会被垃圾回收掉。
- 全局变量: 在页面关闭后结束
- 局部变量: 在执行的作用域块执行完成后结束
- 综上, 局部变量会在其函数块执行之后自动解除,对于引用类型的局部作用域其引用关系会自动解除,大多数的引用类型的全局变量需要手动解除引用关系
下面的例子中const a
这个变量就是被存放在词法环境对象[[Environment]]
中。(下图可以解释为什么要有闭包。)
# 理解垃圾回收问题:
js变量回收规则:
- 在js中定义的全局变量是不会被销毁的,因为随时都可能会用到这个变量,所以不能被销毁。
- 具体引用关系的不会被销毁。
- 如果一个对象不被引用,那么这个对象就会被回收;
- 如果两个对象互相引用,但是没有被第3个对象所引用,那么这两个互相引用的对象也会被回收。
通过下面两段代码进行对比分析:
function a(){
var b= 10;
return function(){
b++;
console.log(num);
}
}
a()(); //11
a()(); //11
分析: 在函数a中返回了一个匿名函数,在这个匿名函数中我们num++了一下,然后在函数外面执行了这个匿名函数函数,现在num是11,然后又执行了一次这个函数,你们应该是12吧,为什么不是呢?
原因: js为了让没有必要的变量保存在内存中,(我们写的任何变量都是需要内存空间的)在不需要这个变量的时候它就会被销毁。所以每次执行一遍 a()() 则变量b就会被销毁。下次再执行,就会重新声明变量b,所以两次输出都是11。
function a(){
var b = 0;
return function(){
b ++;
console.log(b);
}
}
var d = a();
d();//1
d();//2
原因分析:
- 函数a 被 变量d 引用,更准确的说是 函数a 里面的 匿名函数 被变量d所引用。
- 因为变量d 保存的是函数a执行完成后的值,而函数a执行完,返回了那个匿名函数,所以变量d等于匿名函数。 -匿名函数因为使用了 函数a 中的 变量b 并且还被 变量 d所引用,所以就形成了一个闭包。
- 只要这个变量d不等于null的话,那么那个变量b会一直保存到变量d中不会被销毁。
- 所以两次执行的结果不一样
# 垃圾回收问题
# 1.什么是js的垃圾回收机制?
答:javascript中垃圾收集机制是自动回收的,不用人工操作,这让我们更专注于编辑代码上。 回收垃圾机制是定时执行的,具有周期性。
# 2.什么时候会有垃圾?
答:在作用域中当整个作用域中的代码执行完后,作用域中的变量和方法都会没用,此时就是被当做垃圾了。比如局部作用域,一个函数执行完,里面的变量就可以被销毁,其占用内存被释放。
# 3.垃圾回收方式?
答:常用的是标记清除:这样操作:一个变量-->进入环境(被标记,有此标记为不能被清除)-->执行-->离开环境(被标记,这个标记告诉机制能被清除)-->回收机制一段周期后,变量被清除。
# 4.为什么还要管理内存?
答:分配给web浏览器的内存比桌面的应用的内存少,这是出于安全考虑,为了防止运行js的网页耗尽系统内存导致系统崩溃。所以,开发者发现一旦数据不再用时,就将其值设为null来释放其引用(解除引用),这做法适用于全局变量和全局对象属性。
参考资料: