欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

深入刨析闭包

程序员文章站 2022-07-14 14:28:10
...

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);
}