【前端重温系列】闭包及其涉及知识点的理解

【前端重温系列】闭包及其涉及知识点的理解

时间已经是2020年了,马上也就2021年了,现如今的发展,ES6的实现几乎现代化浏览器都实现了。红宝书也出第四版了,删除了过旧的知识,引入了ES6,涵盖至ECMAScript 2019新标准。新的基础工具书的推出,自己也该好好重温一遍基础,重新梳理自己脑海的知识点,过时的删除,腾出空间给新知识。当然,内容主要还是从核心知识开始,扩展性涵盖其他知识点。

今天开始从闭包及其涉及知识点开始说起。

闭包的定义:闭包是什么

不同资料对闭包的解释

MDN

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

阮一峰

其理解:闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

维基百科

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

在支持头等函数的语言中,如果函数f内定义了函数g,那么如果g存在自由变量,且这些自由变量没有在编译过程中被优化掉,那么将产生闭包。

闭包和匿名函数经常被用作同义词。但严格来说,匿名函数就是字面意义上没有被赋予名称的函数,而闭包则实际上是一个函数的实例,也就是说它是存在于内存里的某个结构体。如果从实现上来看的话,匿名函数如果没有捕捉自由变量,那么它其实可以被实现为一个函数指针,或者直接内联到调用点,如果它捕捉了自由变量那么它将是一个闭包;而闭包则意味着同时包括函数指针和环境两个关键元素。在编译优化当中,没有捕捉自由变量的闭包可以被优化成普通函数,这样就无需分配闭包结构体,这种编译技巧被称为函数跃升。

自我理解总结

包含了既不是函数参数、也不是函数的局部变量,而是一个不属于当前作用域的变量,相对于当前作用域来说,是一个自由变量的函数,就叫闭包。

闭包包含了自由变量和函数环境

为什么需要闭包,闭包优缺点

闭包作用

  • 可以引用外部函数的变量或者参数
  • 使该变量或者参数常驻内存,避免被垃圾回收机制所回收

在 js 中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁

总结:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数访问定义时的词法作用域

注意点

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

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包涉及知识:作用域及作用域链

作用域分为词法作用域和动态作用域,Javascript的作用域遵循的就是词法作用域模型

关于词法作用域和动态作用域区别

词法作用域

  • 也称为静态作用域。这是最普遍的一种作用域模型
  • 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸

动态作用域

  • 相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等
  • 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸

词法作用域及其作用域链

词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结如下。

  • 执行上下文(作用域)分全局上下文(全局作用域)、函数上下文(局部作用域)和块级上下文(块级作用域)。

  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。

  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。

  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。

  • 变量的执行上下文用于确定什么时候释放内存。

  • 整个代码结构中只有函数可以限定作用域(待考证)

  • 作用域规则优先使用变量提升规则分析

  • 如果当前作用规则中有名字了, 就不考虑外面的名字

闭包涉及知识点:js内存管理

JavaScript是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。JavaScript的垃圾回收程序可以总结如下。

  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript引擎不再使用这种算法,但某些旧版本的IE仍然会受这种算法的影响,原因是JavaScript会访问非原生JavaScript对象(如DOM元素)。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。

闭包的实现

若函数作为参数被传递

1
2
3
4
5
6
7
8
9
10
11
// 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}

const a = 100
function fn() {
console.log(a)
}
print(fn) // 100

函数作为返回值被返回

1
2
3
4
5
6
7
8
9
10
11
// 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a)
}
}

const fn = create()
const a = 200
fn() // 100

闭包的应用

实现数据(变量和方法)私有化

函数柯里化(函数式编程

闭包相关例题

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
for (var i = 1; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000 * i)
}
console.log(i)

// 5 5 5 5 5 5
// 创建的5个setTimeout闭包共享一个词法作用域,优先打印外层5
// 闭包只能取得包含函数中任何变量赋值最后一个值 // 5


for (var i = 0; i < 5; i++) {
((j) => {
setTimeout(() => {
console.log(j)
}, 1000 * j)
})(i)
}
// 0 1 2 3 4
// 5个setTimeout闭包有自己独立的词法环境
// 闭包读取到不同的i值

var output = function (i) {
setTimeout(function() {
console.log(i);
}, 1000);
};

for (var i = 0; i < 5; i++) {
output(i);
}

// 0 1 2 3 4
// 这里的 i 被赋值给了 output 作用域内的变量 i

// 变种
function test (){
var num = []
var i

for (i = 0; i < 10; i++) {
num[i] = function () {
console.log(i)
}
}

return num[9]
}

test()()
// 10

var test = (function() {
var num = 0
return () => {
return num++
}
}())

for (var i = 0; i < 10; i++) {
test()
}

console.log(test())
// 10


var a = 1;
function test(){
a = 2;
return function(){
console.log(a);
}
var a = 3;
}
test()();
// 2
// 变量提升,test a提升了。后来又赋值2
// 我们作用域的划分,是在书写的过程中,根据你把它写在哪个位置来决定的。像这样划分出来的作用域,遵循的就是词法作用域模型。这里我们匿名函数被定义的时候 a = 3 的赋值动作还没有发生(只有声明会被提前!),因此它拿到的 a 就是 2!


function foo(a,b){
console.log(b);
return {
foo:function(c){
return foo(c,a);
}
}
}

var func1=foo(0);
func1.foo(1);
func1.foo(2);
func1.foo(3);
var func2=foo(0).foo(1).foo(2).foo(3);
var func3=foo(0).foo(1);
func3.foo(2);
func3.foo(3);
// undefined
// 0
// 0
// 0
// undefined
// 0
// 1
// 2
// undefined
// 0
// 1
// 1
// {foo: ƒ}

【前端重温系列】闭包及其涉及知识点的理解

https://blog.vadxq.com/article/frontend-revisited-closure/

作者

vadxq

发布于

2020-10-06

更新于

2020-10-12

许可协议

评论