路漫漫其修远兮
头像

codermjy

A programmer who subconsciously views himself as an artist

will enjoy what he does and will do it better

掌握JavaScript中让人迷惑的闭包

目录

JavaScript中闭包的定义

维基百科中关于闭包的定义

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures)。

是在支持 头等函数 的编程语言中,实现 词法绑定 的一种技术。

闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。

环境里是若干对符号和值的对应关系,它既要包括 约束变量(该函数内部绑定的符号),也要包括 自由变量(在函数外部定义但在函数内被引用)。

闭包跟函数最大的不同在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。

MDN中关于闭包的定义

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是 闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

看完上边维基百科和 MDN 对闭包的解释。是不是一脸懵逼?什么是词法绑定?头等函数?自由变量又指的是什么?

但我们从以上两个官方的解释来看,能统一得出一点结论:

闭包是一个结构体,其实现是函数。

想要理解闭包,首先需要知道两个前提知识点:1. 函数的执行过程,2. 内存管理

JS中的一等公民——函数

何为一等公民?c语言中的抽象数据模型 类(class)、Java8之前的对象(Object)、这些都是在各自语言中的一等公民。它们都有一个特性:使用灵活,能够在程序中无限使用的对象,作为函数时的参数传递,以及作为函数的返回值。

在 JavaScript 中,函数是非常重要的,并且是一等公民:

  1. 函数可以作为另外一个函数的参数。
  2. 函数页可以作为另外一个函数的返回值。

JS中函数的使用是非常灵活的,具体我们用几个案列来解释:

  1. 将函数作为另外一个函数的参数。
function foo(baz){
	baz()
}
function bar(
	console.log('bar')
)

foo(bar)
  1. 函数作为函数的返回值,js 允许函数内部再定义函数。
function foo() {
	function bar() {
		console.log('bar')
	}
	
	return bar
}

var fn = foo()
fn()
  1. 数组中的高阶函数
/*
高阶函数:一个函数如果接收另一个函数作为参数,或者该函数会返回另外一个函数作为返回值的函数,那么这个函数就称之为是一个高阶函数。
*/
var nums = [10, 7, 9, 100, 11]
var newNums = nums.filter((item) => {
  return item % 2 === 0 // 偶数
})

console.log(newNums) // [10, 100]
console.log(nums) // [10, 5, 11, 100, 55]

有了以上对函数的理解。我们再来看看 js中的内存管理:垃圾回收机制。

JS的内存管理

内存结构

JavaScript会在 定义变量时 为我们分配内存。

但是内存的分配方式是一样的吗?

JS 对于基本数据类型内存的分配会在执行时,直接在栈空间进行分配;

JS 对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并将这块空间的指针返回值变量引用;

例如以下代码在内存中的结构:

var name = "mjy"
var age = 18

var obj = {
	name: 'mjy',
	job: 'coder'
}

function foo() {
	console.log('hello')
}

内存管理:垃圾回收机制

因为内存大小的有限,所以当内存不再被需要的时候,我们就要对其进行内存释放。以便腾出更多的空间。

手动管理内存的语言中,如 c:通过 malloc 来进行内存的申请,通过 free 进行内存的释放。

但是这种方式十分的低效麻烦,而且一不小心使用完后就会忘记释放。

所以大部分现代编程语言都有自己的垃圾回收机制

垃圾回收的英文是:Garbage Collection,简称 GC。

对于不再使用的对象,我们都称之为垃圾。它需要被回收掉。

在 JavaScript 引擎中,有着自己的垃圾回收算法:

常见GC算法

1. 引用计数法:

​ 当一个对象有一个引用指向它时,那么这个对象的引用就 +1,当一个对象的引用为 0 时,这个对象就可以被销毁掉,也就是可以被GC回收掉。

​ 这个算法有一个很大的弊端:循环引用,导致无法被正常回收。

2. 标记清除法(运用最广):

​ 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根对象开始,找所有从根对象开始有引用到的对象,对于那些没有引用到的对象,就认为是不可达(不可用)的对象。GC就会对其进行回收。

​ 这个算法可以很好的解决循环引用产生的问题:

在有了以上两个知识点(函数执行过程、内存管理)的了解后,我们就可以正式得来了解闭包了

JavaScript中函数的执行过程

要理解闭包,首先就要理解JavaScript中的函数,以及JavaScript函数的执行过程。可以看我之前写过的这篇文章 理解JavaScript函数执行过程,作用域链。

我们先来看这样的一段代码,在 js 引擎中的执行过程。

function foo() {
    var name = "mjy"
    var age = 18
}

function test() {
 	console.log('test')   
}

foo()
test()

函数解析时

在 js 引擎解析代码时,GO 对象被创建。代码依次解析,当解析到 foo、test 函数时,会在堆内存中为其开辟一块内存空间,内存空间中存放该函数的父级作用域(parentScope)和函数执行体。并将开辟的内存空间地址以址返回的形式存储在 GO 对象的 foo 与 test 中。

函数执行时

开始执行代码,函数执行上下文入栈,并生成函数AO对象,依次执行函数中的代码。

函数对象销毁

当foo函数执行完毕,函数执行上下文出栈,foo函数的对象AO销毁。

闭包的产生

在理解了上面函数的执行过程后,我们来看下面这样一个函数。

function foo() {
	var name = "mjy"
	var age = 18

	function bar() {
		console.log(name)
		console.log(age)
	}
	
	return bar
}

var fn = foo()
fn()

当foo函数执行时,foo函数创建 AO 对象。foo中代码进行解析(当函数确定要被执行时,foo中的代码才会进行解析),foo函数体代码解析到bar函数,并在内存中为bar开辟内存空间。

解析完毕,执行 return bar 代码,将bar函数的内存空间地址,以址返回的形式赋值到变量fn中,fn = foo()

当foo函数执行完毕时,本该销毁的fooAO对象并不会被消耗(也就是不会被GC回收掉)。因为此时 foo 函数的 AO 对象,在根对象GO中是可达的,有变量在指向它(不清楚这一步可以回头看:GC算法--标记清除法)。

这也是闭包会产生内存泄漏的原因。

foo函数执行完毕出栈后,test函数入栈。开始执行生成test函数AO对象,并解析后依次执行bar函数体中的代码:console.log(name)

bar沿着函数作用域链查找要打印的值 name 首先会在自身AO对象中查找。如果未查找到,将会沿着父级作用域继续查找,在父级作用域foo函数的AO中对象查询到 name 属性,将其打印。

理解总结

以上就是一整个闭包的形成过程了。我们现在再回过头来看 维基百科、MDN中对闭包的定义:

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包>>。(function closures)。

是在支持 头等函数 的编程语言中,实现 词法绑定 的一种技术。

闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。

环境里是若干对符号和值的对应关系,它既要包括 约束变量(该函数内部绑定的符号),也要包括 自由变量(在函数外部定义但在函数内被引用)。

闭包跟函数最大的不同在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是 闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

现在看来是不是就很清晰了。

在理解了闭包后,你会发现 维基百科 和 MDN 中对闭包的解释定义是十分准确的(官方喜欢用名词来解释名词,因为这样才能更准确得表达)。

这里摘抄coderwhy老师对闭包的理解:

  • 一个普通的函数function,如果它可以访问外层作用域的自由变量。那么这个函数就是一个闭包。
  • 从广义的角度来说:JavaScript 中的函数都是闭包。
  • 从狭义的角度来说:JavaScript 中的一个函数,如果访问了外层作用域的变量,那么它就是一个闭包。
posted @ 2022-05-20 20:29  不愿染是与非  阅读(55)  评论(0编辑  收藏  举报