闭包

本质: 局部数据共享

没有任何一个技术方案可以做到局部数据共享。那么如何局部共享呢?
可以创建一个函数作用域,在这个作用域内共享数据:

1
2
3
4
5
6
7
8
9
10
11
function p () {
var a = 10

funtion foo () {
console.log(a)
}

funtion bar () {
console.log(a)
}
}

p 函数的局部变量 a,被 foo 和 bar 这两个函数共享

定义: 闭包是一个特殊的对象。它由两部分组成,执行上下文 A 以及在 A 中创建的函数 B。当 B 执行时,如果访问了 A 中的变量,那么闭包就产生了。

在 chrome 中,执行上下文 A 的函数名代指闭包

词法作用域:在我们编写代码时,语法规范就已经确定了词法作用域的作用范围。

词法作用域是为了明确告诉我们,当前的上下文环境中,能够访问哪些变量参与程序的运行。除了自身上下文,还可以从函数体的[[Scopes]]属性访问其他作用域的声明。

闭包形成的原因:

闭包是基于词法作用域的规则产生,让函数内部可以访问函数外部的声明。闭包在代码解析时就能确定。

闭包对象什么时候被回收

闭包对象[Closure(A)]的引用存在于函数体 B 的内存中。如果 B 函数体被回收,闭包对象同样也被回收。

看个例子:

1
2
3
4
5
6
7
8
var fn = function () {
var a = 1;
(function f() {
console.log(++a); // 这时候闭包产生了,
})();
console.log(a); //运行到这里是,f的执行上下文被销毁,闭包也消失了。此时fn中的a变成了2
};
fn(); // 等fn执行完,fn的执行上下文也被销毁

pic.1708325490489
内部函数f执行完毕,其执行上下文被回收,闭包对象也随之被回收。
pic.1708325496558

保存闭包对象不被回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
var a = 20;

function bar() {
a = a + 1;
var b = 10;
console.log(a + b);
}

return bar;
}

foo()(); // 31
foo()(); // 31

foo 调用完毕,其执行上下文被回收,bar 作为 foo 的一部分,自然也被回收,那么保存在 foo.[[Scipes]] 上的闭包对象自然也被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
var a = 20;

function bar() {
a = a + 1;
var b = 10;
console.log(a + b);
}

return bar;
}

// foo()()
// foo()()

var bar = foo();
bar(); // 31
bar(); // 32

那么如何保存这个闭包对象不被回收呢?我们知道垃圾回收机制会通过根搜索算法回收非活跃的对象,那只需要在当前执行上下文中,保存内部函数的引用,闭包对象就不会被回收。

当 foo 执行完毕时,foo 依然会被回收,但是由于执行了var bar = foo(),内部函数 bar 有了新的方式保存引用,所以即使 foo 执行完毕,bar 也不会被回收,而是在内存中持续存在,那么闭包对象 Closure[foo]也就不会被回收。

闭包的应用

常用面试题

表现为 setTimeout 第二个参数为 1-5,定时器在循环结束后隔秒输出 6

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}

用自执行函数改造-隔秒一次输出 1-5
自执行函数与 timer 形成闭包,所以 i 被保存了下来

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i);
}

pic.1708325509698
可以看到,循环执行完毕后,还能访问到 i,此时 i 的值为保存在闭包对象的值。

进一步改造,缩小约束范围,只约束 timer,瞬间输出 1-5

1
2
3
4
5
6
7
8
for (var i = 1; i <= 5; i++) {
setTimeout(
(function timer(i) {
console.log(i);
})(i),
i * 1000,
);
}

单例模式与闭包

单例模式,就是只有一个实例的对象
对象字面量的方法,就是一个单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
var per = {
name: 'Jake',
age: 20,
getName: function () {
return this.name;
},
getAge: function () {
return this.age;
},
};

per.name = 'Tom';
console.log(per.getName());

这样的单例存在一个问题,属性可以被外部修改。
我们希望拥有自己的私有方法/属性,所以可以利用自执行函数的作用域进行隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
var per = (function () {
var name = 'Jake';
var age = 20;
return {
getName: function () {
return name;
},

getAge: function () {
return age;
},
};
})();

这样内部修改就不会被修改,我们可以控制对外提供的属性和方法
但是还有一个小问题,这种方式一开始就被初始化了。

改造一下,仅在调用时初始化

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
var per = (function () {
var instance = null; // 保存实例
var name = 'Jake';
var age = 20;

function initial() {
return {
getName: function () {
return name;
},
getAge: function () {
return age;
},
};
}

return {
getInstance: function () {
if (!instance) {
instance = initial();
}
return instance;
},
};
})();

var per1 = per.getInstance();
var per2 = per.getInstance();

console.log(per1 === per2); // true

模块化与闭包

为了减少全局变量的使用,可用模块化的思维解决需要使用全局变量的问题

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 使用单例模式,创建一个用于管理全局状态的模块
var module_status = (function module_status() {
var status = {
number: 0,
color: null,
};

var get = function (prop) {
return status[prop]; // 访问闭包对象 Closure(module_status) 的属性
};

var set = function (prop, value) {
status[prop] = value; // 设置闭包对象 Closure(module_status) 的属性
};

return {
get: get,
set: set,
};
})();

// 负责改变颜色的模块
var module_color = (function module_color() {
// 假装用这种方式引入模块 类似于 import state from 'module_status';
var state = module_status;
var colors = ['orange', '#ccc', 'pink'];

function render() {
var color = colors[state.get('number') % 3]; // 访问闭包对象 Closure(module_status) 的 number 属性
document.body.style.backgroundColor = color;
}

return {
render: render,
};
})();

// 负责显示当前number的模块
var module_context = (function module_context() {
var state = module_status;

function render() {
console.log('this Number is '); // 小提示(与主逻辑无关): render[[scopes]]]作用域信息在解析这个函数时就已经确定了。执行到这里时 Closure(module_context) 就已经产生了
document.body.innerHTML = 'this Number is ' + state.get('number'); // 执行闭包对象 Closure(module_status) 的 get 方法,
}

return {
render: render,
};
})();

// 主模块
var module_main = (function module_main() {
var state = module_status; // 保存 module_status 模块返回的字面量对象的引用
var color = module_color;
var context = module_context;

var newNumber = state.get('number') + 1; // 访问闭包对象 Closure(module_status) 的 number 属性
// state.get() 执行完毕,正常情况下闭包对象 Closure(module_status) 随内部函数的执行完毕而被回收。
// 但是这里将闭包对象的引用地址保存在当前执行上下文中,所以闭包对象 Closure(module_status) 并没有被销毁
state.set('number', newNumber); // 改变闭包对象 Closure(module_status) 的number属性,number == 1

context.render(); // 43行访问的闭包对象于77行改变的闭包对象是同一个对象
color.render();
})();

// 注意,module_status 的引用一直存在全局对象中没被释放,所以执行 module_status 时,还能访问到闭包对象Closure(module_status),以为输出1
console.log('-------', module_status.get('number'));

// 主模块
// var module_main = (function () {
// var state = module_status;
// var color = module_color;
// var context = module_context;

// setInterval(function () { // 这里使用定时器实现切换固定颜色的功能
// var newNumber = state.get('number') + 1;
// state.set('number', newNumber);

// color.render();
// context.render();

// console.log(1);
// }, 1000);
// })();

其他应用场景

使用其他模块暴露的接口

utils/index.js

1
export const a = 1;
1
2
3
4
5
6
7
import { a } from '../../../utils';

const Closure = () => {
console.log(a);
return <div>{a}</div>;
};
export default Closure;

在函数组件 Closure 中访问了 utils 模块中的变量 a,那么闭包对象就产生了。
pic.1708325527266

父子组件通信

pic.1708325535782
<Child />组件中执行foo时,闭包对象Closure(MyClosure)产生了。

hooks 缓存上次结果

本质是将上次的结果(闭包对象)缓存在函数体内
pic.1708325542236
另外还有以下关于闭包的使用

  • 高阶函数: 记忆函数
  • 状态管理:redux、mbox
  • 函数封装
  • BEM
  • vue scoped
  • css in js:借 js 的作用域,生成 css 作用域
  • css modules

闭包的优缺点

优点:

  1. 可以重复使用闭包对象内的变量,并且不会造成变量污染
  2. 可以用来定义私有属性和私有方法(单例模式与闭包)

缺点:

  1. 可能会导致堆内存消耗过大

闭包对象的回收

  • 如果是全局变量保存了内部函数的引用,如果没有手动释放这个对象,闭包对象会一直存在堆内存中
  • 如果是函数内的一个局部变量保存了内部函数的引用,函数出栈后如果没有引用了,闭包对象等待下一次垃圾回收器回收
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×