深入刨析闭包
1. 从作用域开始
程序的一端代码在执行的时候一般会经历三个步骤
- 分词(词法分析)
- 解析(语法分析)
- 代码生成:即将抽象语法树转为可执行代码
认识一下三个大哥
- js引擎:负责js程序的编译及执行过程
- 编译器:负责语法分析及代码生成
- 作用域: 收集并维护由所有声明的标识符(变量) 组成的一系列查
询, 并实施一套非常严格的规则, 确定当前执行的代码对这些标识符的访问权限。
回想一下,每一种编程语言最基本的能力就可以存放变量。那么它是如何存放的呢,程序执行时又是怎样寻找他们。这时就要有一套规则用来规范这一套流程。规范这一套流程的规则就是作用域
从一段代码入手
var a=2
编译器的工作:遇到var a,它会询问作用域当前作用域下是否存在一个变量a,存在则忽略不存在创建
然后编译器生成可执行代码供引擎使用,这个代码是用来处理a=2的赋值操作,引擎执行会问作用域:当前域下有没有一个叫a的变量,有就使用没有继续找
总结一下这里:
var a(在编译时进行处理)
a=2(在运行时进行处理)
LHS 查询与 RHS 查询
简单理解,一个变量出现在赋值操作的左侧时引擎会为变量进行LHS查询。出现在右侧进行RHS查询
如上面的a=2
,引擎就对变量a进行了LHS查询。
再准确的理解就是:LHS查询目的是要找到这个变量的容器本身,从而改变里面的值
;RHS查询它的目的就是找这个变量里的值
作用域链
作用域是寻找变量的一套规则,且实际中,通常要兼顾多个作用域。
如:
function foo(){
console.log("demo")
}
foo()
这段代码中就有两种作用域,全局和函数。有了作用域与作用域的嵌套即就产生了作用域链
。
js中的作用域种类:
- 全局作用域
- 函数作用域
- 块作用域
作用域中的变量寻找:
function foo(){
var a=10;
function bar (){
console.log(a)
}
}
foo()
在bar的函数作用域里面js引擎会向当前所处作用域询问是否有a这一个变量,没有取到js引擎会向它的父级作用域foo接着询问,有则使用没有接着向外去找。直到全局中,如若均没有找到则停止查找。
2. 词法作用域
作用域共有两种主要的工作模型
- 词法作用域
- 动态作用域
那么什么是词法作用域呢?
词法作用域就是定义在词法阶段的作用域,说的更直白点就是由你的书写位置
确定的
词法作用域的修改
因为不推荐使用,故不写案例代码了。欺骗语法方法如下
-
eval
-
with
3. 作用域闭包
闭包的定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使这个函数是在当前词法作用域之外执行
。
再来说的直白一点,闭包就是一个有权限访问其所在词法作用域中变量的一个函数。
通过代码片段理解:
function foo(){
var a=3;
function bar(){
console.log(a);
}
return bar;
}
const a=foo()
a()
foo函数执行之后返回值是bar函数,把bar函数传给变量a并进行调用。引用类型的特性我们知道bar和a本就是指的同一个函数。也就是说bar函数在自己词法作用域以外的地方被执行了。
一般一个函数执行后,它的整个内部作用域都会被销毁,因为js引擎的垃圾处理机制。但是这里的foo函数明显没有垃圾处理。
闭包的主要功能就是在这,因为bar函数仍要对此块作用域进行引用。使得这块本应消失的空间被保存了下来
再来看书中一个栗子:
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀, 这就是闭包!将baz函数所在的作用域及其作用域链上的信息均保存了下来
}
闭包随处可见,基本上任何一个有回调的函数都是闭包。
模块模式
模块也非常容易理解,它的代码形式就是一个外部函数执行可以返回它的内部函数。
function foo(){
var a=2;
return function bar(){
console.log(a);
}
}
foo()();
模块模式需要两个必要的条件
- 必须有外部封闭函数,且它至少被调用一次
- 该封闭函数至少返回一个内部函数
4. 闭包存在的优缺点
优点
- 阻止一些词法作用域的回收,保存一些有用信息,模拟一个块级作用域
缺点
- 可以说闭包的优点也是它的缺点,因为他会保存一些信息始终在内存中。故如果出现过多的闭包会导致内存泄漏
5. 常见的闭包相关问题
比较经典的闭包与循环结合
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
它的执行结果为5,5,5,5,5
其实很容易理解,同步代码先执行。每次循环都是将一个setTimeout()放到一个宏任务队列中去。在主执行栈中同步代码执行完毕之后i的值为5,这是宏任务队列的函数开始进栈。故根据作域链查到的i的值都是5了。
这段程序没有按照我想得到的结果,它的主要原因是5个setTimeout共用的是一个i
那么不让他们共用一个就好了
改造1
每次循环再创建一个作用域,且因为闭包的关系使得setTimeout从宏任务队列处理执行时仍能访问到它所在词法作用域中的数据
for (var i = 0; i < 5; i++) {
(function(){
var j=i;
setTimeout(function() {
console.log(j);
}, 1000);
}())
}
改造2
上面使用的函数作用域,这次使用块级。let可将变量绑定到当前块级作用域下
for (var i = 0; i < 5; i++) {
let j = i;
setTimeout(function() {
console.log(j);
}, 1000);for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(j);
}, 1000);
}
}
一个知识点:for循环头部的let声明有一个特殊行为 , 这个行为指出变量在循环过程中不止被声明一次, 每次迭代都会声明。 随
后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
即可以简化成这样:
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(j);
}, 1000);
}
改造3:与闭包无关了
利用 setTimout 的第三个参数,将没一轮的i保存下来
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
上一篇: 数据结构——数组模拟实现队列
下一篇: LHS和RHS查询笔记