搞懂Javascript闭包概念(二)

这是闭包系列的第二章,第一章只是囫囵吞枣般的记笔记,根本没多深入理解,最近在看了MDN上关于闭包的文章以及B站几个闭包视频后有了些许理解,于是用小本本记下来。

首先我们先来理解几个概念

作用域

作用域就是变量与函数的可访问范围。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。

1
2
3
4
5
6
7
8
9
10
11
12
let a = 1; // 定义一个全局变量,全局变量在js文件任意位置均可使用(在chrome的控制台也是可以输出的),会一直存在,直到网页也就是window对象关闭后释放
console.log(a); // 1

function fn() {
let b = 2;
console.log(a) // 函数内部输出了a,此时,找不到a,按照作用域链特性向外层寻找,找到了a为‘1’;所以结果输出的是1
console.log(b) // 2
}
console.log(b) // b is not defined,执行fn函数计算机会为函数开辟一个新的内存地址,在函数内部创建变量并赋值为2,函数执行完即释放,所以在函数外部不可访问
fn()
fn() // 调用多次即会生成多个内存地址

作用域链

上面的代码形象的解释了全局作用域和局部作用域这两个概念。接下来看看下面这个例子。

1
2
3
4
5
6
7
8
9
function fn() {
var a = 0;
function sum() {
console.log(++a)
}
sum();
}
fn(); // 1
fn(); // 1,因为上面提到,每次调用是新开辟内存地址,也是重新给a赋值为0,所以不管调用多少次,结果都是1

那么,有没有什么办法可以实现每次调用fn,输出值就自增1呢,我们把代码修改一下

1
2
3
4
5
6
7
8
9
10
function fn() {
var a = 0;
return function sum() {
console.log(++a)
}
}
let test = fn();
test(); // 1
test(); // 2
test(); // 3

为什么每次调用都会自增而不是一直输出1呢,我们来分析一下上面的代码:

  1. 执行fn函数而fn函数的返回值还是一个函数就相当于将sum函数赋值给一个全局变量test,全局变量我们上文提到过,只有当网页关闭时才会卸载,这就导致sum函数始终在内存中。
  2. 那么此时的test是什么呢,我们来打印一下看看,输出如下
1
2
3
ƒ sum() {
console.log(++a)
}
  1. 每次执行test()即相当于执行sum函数,++a则相当于a=a+1,代码可简化下
1
2
// test() 相当于执行一次  a = a + 1
// test() 相当于再执行一次 a = a + 1
  1. 因为sum一直在内存中,而fn是sum的父函数,sum的存在依赖于fn,因此fn也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收,所以整个fn函数内部数据都会被保留,a自增的结果也就一直存在

仔细回想一下刚刚我们查找变量a的过程发生了什么?

  • 先从当前上下文的变量对象中查找
  • 如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找
  • 一直找到全局上下文的变量对象,也就是全局对象(ps:作用域链的顶端就是全局对象)

这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

课外小拓展:fn函数中不return函数,直接return变量a行不行

答案是:不行,上代码,每次调用完fn,内部的变量a就会被释放,所以每次调用都是返回1

1
2
3
4
5
6
7
8
function fn() {
var a = 0;
return ++a;
}
let test = fn();
console.log(test) // 1
console.log(test) // 1
console.log(test) // 1

主角登场-闭包

面试题问什么是闭包

现在我们来尝试回答一下:

  1. 内部函数可以访问外部函数的变量称之为闭包
  2. 闭包就是能够读取其他函数内部变量的函数,在本质上是函数内部和函数外部链接的桥梁
  3. 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
  4. 函数和对其周围状态(词法环境)的引用捆绑在一起构成闭包(closure)(ps:回答最官方最靠谱的一个)

利用闭包搞点事

给定一个数组,里面有若干数字,要求返回数字大于3且小于9的

1
2
3
let arr = [1, 32, 56, 7, 1, 2, 5, 7, 3, 4, , 44];
let a = arr.filter(item => item >= 3 && item <= 9);
console.log(a) // [7, 5, 7, 3, 4]

一顿操作轻轻松松实现了需求,如果此时万恶的pm改了一下需求,要求返回4-8的呢?既然我们目前在学闭包,那就利用闭包的特性来实现一下

1
2
3
4
5
6
7
8
let arr = [1, 32, 56, 7, 1, 2, 5, 7, 3, 4, 44];
function between(a, b) {
return function (value) {
return value >= a && value <= b;
}
}
let a = arr.filter(between(4, 8));
console.log(a) // [7, 5, 7, 4]

代码是不是更加健壮了呢,我们来分析一下上面的代码

  1. 首先运行between函数而between函数返回一个函数,这个函数作为filter的回调函数来使用
  2. filter循环下,不断执行between中的子函数,因为闭包的特性,子函数可以访问到父函数between函数的参数,然后就不断开辟空间执行return value >= a && value <= b这段代码

闭包中的历史遗留问题

1
2
3
4
5
6
7
8
9
10
11
let hd = {
user: 'test',
get: function () {
console.log(this) // hd的对象
return function () {
return this.user;
}
}
}
let a = hd.get();
console.log(a()) // undefined

根据闭包的特性return this.user;此处的this应该是get函数中的this即hd对象,可为什么输出是undefined
this永远指向调用他的的对象,我们将hd.get()赋值给全局的对象a,所以此处this指向全局变量Window,而Window下是没有user的对象的,所以输出undefined

解决方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 以前经常看到的 var that = this;
let hd = {
user: 'test',
get: function () {
let that = this;
return function () {
return that.user;
}
}
}
let a = hd.get();
console.log(a()) //test

// 第二种方法,箭头函数,箭头函数没有this,默认绑定外层this
let hd = {
user: 'test',
get: function () {
return () => {
return this.user;
}
}
}
let a = hd.get();
console.log(a()) //test

闭包的优点

延长外部函数局部变量生命周期

闭包的一些缺陷

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量手动删除。

1
2
3
// 手动释放f的引用

fn = null

参考资料:
MDN 闭包
JS 作用域及作用域链
前端知识总结(二) 闭包
JavaScript基础(三)作用域


搞懂Javascript闭包概念(二)
https://xypecho.github.io/2021/01/31/搞懂Javascript闭包概念-二/
作者
很青的青蛙
发布于
2021年1月31日
许可协议