本文从作用域和预解析出发,之后再去了解闭包(了解这个可以更好的了解闭包的环境)然后后面就再谈谈下闭包的this指向问题以及和闭包密切相关的变量生命周期啦。

推荐大家看看B站闭包,感觉挺ok的

作用域

在了解闭包之前,我们得首先了要解什么是作用域以及预解析的相关内容

作用域:变量可以起作用的范围

全局变量和局部变量

  • 全局变量

    在任何地方都可以访问到的变量就是全局变量,对应全局作用域

  • 局部变量

    只在固定的代码片段内可访问到的变量,最常见的例如函数内部。对应局部作用域(函数作用域)

没有声明的变量是全局变量。
变量退出作用域之后会销毁,全局变量关闭网页或浏览器才会销毁

块级作用域

任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有let或const变量在代码块外都是不可见的,我们称之为块级作用域。

1
2
3
4
5
6
7
8
let b = 1

{
let a = 2
var c =3
}

console.log(a,b,c) //a:not defined b:1 c:3

for循环作用域

若在for循环中用let定义i,则每次循环都会产生一个块级作用域

1
2
3
for (let i = 10; i >= 0; i--) {
console.log(i)
}

若使用var定义,则为全局作用域

作用域链

只有函数可以制造作用域结构, 那么只要是代码,就至少有一个作用域, 即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。

将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。

1
2
3
4
5
6
7
8
9
10
function f1() {
function f2() {
}
}

var num = 456;
function f3() {
function f4() {
}
}

图源网络

1
2
3
4
5
6
7
8
9
function f1() {
var num = 123;
function f2() {
console.log(num);
}
f2();
}
var num = 456;
f1(); //123

图源网络

预解析

JavaScript代码的执行是由浏览器中的JavaScript解析器来执行的。JavaScript解析器执行JavaScript代码的时候,分为两个过程:预解析过程和代码执行过程

预解析过程:

  1. 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
  2. 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
  3. 先提升var,再提升function。

JavaScript的执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 案例1
var a = 25;
function abc() {
alert(a); //undefined
var a = 10;
}
abc();


// 案例2
console.log(a); //[Function: a]
function a() {
console.log('aaaaa');
}
var a = 1;
console.log(a); //1

这一部分源于很久之前自己学的JavaScript,那个老师讲的很好,就是不知道出处在哪里了。

变量提升

  • 变量提升

    定义变量的时候,变量的声明会被提升到作用域的最上面,变量的赋值不会提升。

  • 函数提升

    JavaScript解析器首先会把当前作用域的函数声明提前到整个作用域的最前面

1
2
3
4
5
6
7
8
9
10
f1();
console.log(c);
console.log(b);
console.log(a);
function f1() {
var a = b = c = 9; //等同于var a = 9; b = 9; c = 9
console.log(a);
console.log(b);
console.log(c);
}

闭包

简介

子函数和其访问其他函数的变量统称为一个闭包,如下图

知乎中的解释:「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。

image-20200806154954104

闭包的作用

作用:闭包常常用来「间接访问一个变量」。换句话说,「隐藏一个变量」。

让变量的值始终保持在内存中

如下面定义了一个数组对象,我么们要挑选价格在1-10之间的商品,于是我们定义了一个函数,a和b是这个函数的变量,我们在里面又定义一个函数去获取这两个变量并求我们需要的值,最终在全局去使用这个值,于是我们就用到了闭包。

这样写的优点:防止变量污染全局(函数作用域),且可以得到函数体内的私有成员(return)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let commodity = [
{
title: 'apple',
price: 10
},
{
title: 'orange',
price: 12
},
{
title: 'egg',
price: 2
},
{
title: 'milk',
price: 6
},
{
title: 'pineapple',
price: 13
},
{
title: 'lemon',
price: 5
}
]

function get(a, b) {
return function(i) {
return i.price >= a && i.price <= b
}
}
console.table(commodity.filter(get(1,10)))//需要f5一下

注意:用到return是因为我们需要到这个函数体内的值,不能说他就是闭包的一部分。

image-20210312153842288

闭包中this的指向问题

下面有这么一段代码

1
2
3
4
5
6
let fruit = {
name: 'apple',
say: function() {
return this.name
}
}

我们来输出这个结果

1
2
let a = fruit.say()
console.log(a)

果不其然,和我们预想的一样,它输出的结果就是apple,因为this指向的是调用该方法的对象,即fruit,所以自然而然地就输出name属性了

然后,让我们来看看下面这个场景,当我们使用了闭包呢,又会发生什么样的现象

1
2
3
4
5
6
7
8
9
10
11
12
let fruit = {
name: 'apple',
say: function() {
return function() {
return this.name
}
}
}

//下面两段代码等同于console.log(fruit.say()())
let a = fruit.say()
console.log(a()) //undefined

是的,没错,输出结果就跟注释一样,是undefined,为什么呢,仔细看看,你会发现,首先我们调用了fruit中的say()方法,然后用a去接收,接着在我们打印a()时,这个时候函数体内的this指向的其实就是window对象(也就是说,我们在全局window对象中调用了a()这个方法),于是乎,它找不到window中的name属性,也就打印出了undefined,这个就是在使用闭包时可能会遇到的问题,也是在箭头函数未出现前会出现的问题

解决方法有两种,如果前面有仔细阅读的话,你就会发现,其中一种就是用箭头函数去解决这个问题,下面放出两段解决方法

  • 方法一

    我们只需要修改两行代码,一个就是接收对象,另一个则为调用这个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let fruit = {
    name: 'apple',
    say: function() {
    var This = this //定义变量接收该对象
    return function() {
    return This.name //使用
    }
    }
    }
  • 方法二(提倡)

    使用箭头函数替代

    1
    2
    3
    4
    5
    6
    7
    8
    let fruit = {
    name: 'apple',
    say: function() {
    return () => {
    return this.name
    }
    }
    }

    变量的生命周期

对于全局变量来说,它们的生命周期是永久的,除非我们主动去销毁它

对于函数作用域下的变量来说,当函数执行完退出后,这些变量就失去了价值,也会随着函数的退出而消失

而在闭包里,同样都是函数,只不过是以嵌套的形式呈现,这个变量就不会被销毁呢

回到前面this的指向问题中的代码例子中去,当我们在执行var a = fruit.say()时,首先它会在执行say()时返回一个匿名函数的引用,它可以访问到a()被调用时产生的环境,局部变量就会一直存在在这个环境里(这里每定义变量可能不是很明显,下面会有另一个例子),而当我们需要频繁调用a()时,这个变量就会一直被用到,那么它就没有被销毁的理由,所以就会一直存在了

1
2
3
4
5
6
7
8
9
10
11
12
//普通函数
var reduce1 = function() {
var b = 1
b--
console.log(b)
}
reduce1() //0
reduce1() //0 重新生成变量,下同
reduce1() //0
reduce1() //0
reduce1() //0
reduce1() //0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   //闭包
var add = function () {
var a = 1
return function() {
a++
console.log(a)
}
}
var add1 = add() //产生一个临时环境存储变量
add1() //2
add1() //3
add1() //4
add1() //5
add1() //6
add1() //7

闭包的更多作用*

可以看看《JavaScript设计模式与开发实践》这本书有关闭包的相关内容

  1. 封装变量
  2. 闭包和面向对象设计
  3. 用闭包实现命令模式
  4. 内存管理