阅读 543

深入刨析闭包

深入刨析闭包

参考书籍=> 你不知道的js

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);
}
复制代码