JavaScript学习笔记——函数 Part7:闭包

闭包(closure)被认为是必不可少的语言特性,,也是所有传统语言都想添加的语言特性。
闭包很难学?实际上我们早就学习过了((((((P6函数环境与词法作用域),只是那时我们没有称之为闭包而已。
ps. 作为闭包的前置知识,务必先学习环境与词法作用域

要点

  • 自由变量指的是在函数体内未绑定的变量
  • 闭包指的是函数及其引用的环境。 闭包捕获其创建时所处作用域内的变量(包括自由变量)的值(将它们保存在环境中)
  • JavaScript中的闭包可以简单理解成【在函数内部定义的嵌套函数】,使用闭包能够读取其他函数内的局部变量,作为连接函数内部和外部的桥梁
  • 闭包的本质源自两点:词法作用域和函数作为值传递。
  • 创建闭包的方法是:
    在某函数的内部创建嵌套函数,且嵌套函数中使用了外围的自由变量
    (自由变量 即 没有在嵌套函数本地定义的变量)
  • 在创建闭包的上下文外部执行它时,闭包函数的自由变量的值由其环境决定
  • 通常使用闭包来为事件处理程序捕获状态
  • 闭包包含的是实时的实际环境,而非环境的(静态的)副本

复习:函数与环境、词法作用域

  • 函数的环境包含了在其作用域内出现所有局部变量(包括在函数内定义的变量、包括没有在函数内定义的变量;包括函数内的嵌套函数、包括传入函数的形参,但不包含全局变量
  • 调用函数时,实际上是在与其相关联的环境中执行这个函数
  • 无论何时调用函数,函数都优先使用其环境中的有定义的变量,若环境中找不到相应的变量,再向外查找同名变量
  • 函数及其环境创建后,无法直接访问(无法改变)环境中的变量值,必须通过那个函数来访问和改变(私有性)
  • 关于函数环境,详见((((((((((((P6

什么是闭包(closure)

  • 我们知道,从函数返回函数时,返回的是函数引用(指向函数及其环境)
  • 如果在函数内部创建嵌套函数,并返回这个嵌套函数,就得到了闭包
    (结合使用闭包的使用场景来说,闭包多指嵌套函数及其环境,该环境包含了嵌套函数外部的变量值)

闭包
图:闭包是函数及其环境,但这不是一般情况下的“函数及其环境”,这里的环境包含了自由变量(即:在函数外部定义的变量,也称外围变量)

闭包:简单地说就是函数和引用环境


闭包(closure)与敲定(close,动词)函数有关

函数内部的变量有两种:

  1. 局部变量:在函数体中定义的变量
  2. 自由变量(本文也称之为“外围变量”):不是在本地定义的变量
    称其“自由”是因为:在函数体内,自由变量没有绑定到任何值(即:它们不是在本地声明的)

有了给每个自由变量都提供了值的环境后,便将函数敲定了;而函数和环境一起被称为闭包。

因此我们对闭包下定义:

闭包 = 含有外围变量的函数 + 为这些外围变量提供了值(提供了变量绑定,将变量绑定到某个值)的环境

可以参照正式的闭包定义,加深理解

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

为什么函数中会出现不是在函数本地定义的变量(自由变量)呢?
这与JavaScript的特性有关,此情况多出现在嵌套函数中,详见((((P6 环境中的例子

为什么说闭包(closure)与敲定(close)函数有关?敲定有什么用?
对于自由变量(未在函数内定义),有两种情况:①它是全局变量;②它是“外层函数”定义的变量(当前的函数是一个嵌套函数)。
函数可以被敲定,就意味着函数内所有自由变量都是情况②(环境中不存储全局变量),意味着函数体内出现的所有变量,都能在其关联的环境中找到,在函数中处理变量时,不会发生“在本地找不到变量,需要查找全局变量”的情况,这契合了使用闭包的目的——利用环境保护局部变量,从而不与任何代码冲突(有利于多人合作开发代码,保证每个人定义的变量不冲突)
因此,函数能否被敲定与闭包的概念息息相关

闭包有什么好处

若不使用闭包,我们创建一个计数器的代码如下:

var count = 0;
function counter() {
	count = count + 1;
	return count;
}

console.log(counter());//输出1
console.log(counter());//输出2
console.log(counter());//输出3

这种做法的缺点是是,使用全局变量count
在代码中我们应该尽量避免使用全局变量
协作开发代码时,大家帝常会使用相同的变量名,进而导致冲突。

如果使用闭包,就能用受保护的局部变量实现计数器。这样,计数器将不会与任何代码发生冲突,且只能通过调用相应的函数(也叫闭包)来增加计数器的值。

用闭包实现“神奇”的计数器

从函数返回的函数携带了其周边环境,可利用这一点来创建闭包

function makeCounter() {
	var count = 0;

	function counter() {
		count = count + 1;
		return count;
	}
	return counter;//从函数返回函数(counter及其环境一同返回)
}
var doCount = makeCounter();
console.log(doCount());//输出1
console.log(doCount());//输出2
console.log(doCount());//输出3

分析(很关键!):

var doCount = makeCounter();

我们调用makeCounter,它创建函数counter,并将其与包含自由变量count的环境一起返回。换句话说,它创建了一个闭包。从makeCounter返回的函数存储在doCount中。

  • 从函数返回函数,实际上返回的是函数引用,此函数引用指向函数及其环境
    因此,这里makeCounter()返回一个函数引用,指向counter函数及其环境
  • 函数的环境保存了在函数中出现所有局部变量(全局变量除外)
    因此,这里与counter函数关联的环境中存储了count变量的值
  • 根据词法作用域,count变量未在本地定义,因此环境保存的count源自于“外层”函数的count变量,其值为0
console.log(doCount());//输出1

我们调用函数doCount,这将执行函数counter的函数体。
遇到变量count时,我们在环境中查找并获取它的值。我们将count的值加1,将结果存回到环境中,再将结果返回到调用doCount的地方。

  • 函数及其环境创建后,无法直接访问(无法改变)环境中的变量值必须通过那个函数来访问和改变(私有性)
    因此,这里通过doCount调用counter时(它们实际上指向的是同一个函数),其环境中的count变量的值会改变(+1),并且没有其他方法能改变count的值
  • 最终doCount函数返回值为count(count=1)
console.log(doCount());//输出2
console.log(doCount());//输出3
  • 后两次与上面同理

总结:
调用makeCounter时,我们获得的是一个闭包:一个函数及其环境(保存了外围变量count)
调用doCount(指向函数counter的引用),进而需要获取count的值时,我们使用这个闭包的环境中的变量count。在外部世界(全局作用域中),根本看不到变量count,但调用doCount时可以使用它,此外没有其他任何办法能够获取count。

对闭包的理解

闭包就是[能够读取其他函数内的局部变量的函数]
本质上,闭包就是连接函数内部和外部的桥梁

在JavaScript中,只有函数内的嵌套函数才能读取函数的局部变量;
因此JavaScript中的闭包可以简单理解成【在函数内部定义的嵌套函数】


JavaScript闭包的本质源自两点,词法作用域和函数作为值传递。

代码解释执行时按照词法作用域的规则,可以访问外围变量,这些外围变量(相对于嵌套函数而言就是“自由变量”)保存在环境

对于嵌套函数的环境,自由变量值(外围变量)在外层函数执行时创建,外层函数执行完毕时理应销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性
并且,因为自由变量(外围变量)不是在函数内定义的,却保存在函数的环境,导致在外部世界(全局作用域中),根本看不到这些变量(自由变量),除了调用该函数,没有其他任何办法能够获取这些自由变量

闭包不是“封闭内部状态”,而是“封闭外部状态”:当外部状态的scope失效时,其状态还保存了一份在内部状态中(即:外围变量的值保存在嵌套函数的环境中)

「闭包是词法作用域的体现」

因为闭包可以大大的增强函数的处理能力,函数可以作为first-class的这一优点才能更好的发挥出来。

创建闭包

怎样创建一个闭包?

  • 注意定义,闭包指函数使用了自由变量,并且将它们保存在环境中(这意味着这些变量不是全局变量,即他们是外围变量
    因此要创建闭包,要在函数内部创建嵌套函数且使用外围变量,从而产生闭包
  • 另一种理解:闭包的核心在于,函数的环境中保存了自由变量的值,从而得到了受保护的局部变量(并且它们只能通过调用函数来访问,不能直接访问)。
    可见,创建闭包,要在嵌套函数中使用外围变量的值(这些外围变量在外层函数中定义)

综上,创建闭包的方法是:
在某函数的内部创建嵌套函数,且嵌套函数中使用了外围的自由变量
(自由变量 即 没有在嵌套函数本地定义的变量)

法一 从函数返回函数来创建闭包

function makeMultiplierN(n) {
	return function multBy(m) {//返回一个函数,它会将传入的参数乘以n
		return n*m;
	};
}
var multBy2 = makeMultiplierN(2);
console.log("3 multiply by 2 = " + multBy2(3));

法二 将函数表达式用作实参,创建闭包

function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage

//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
	setTimeout(function() {
		alert(doneMessage);
	}, n);
	
}
makeTimer("Cookies are done!", 1000);

法三 用事件处理程序创建闭包

网页上有两个元素:
<button id="clickme">Click me!</button><div id="message"></div>
每单击一次按钮,<div>的内容显示为用户单击按钮的总次数

不使用闭包的代码如下:

var count = 0;
//count必须声明为全局的,因为如果将其作为handleClick的局部变量,
//则用户每次单击时,它都将被初始化。
window.onload = function() {
	var button = document.getElementById("clickme");
	button.onclick = handleClick;
};

function handleClick() {
	var div = document.getElementById("message");
	count++;
	div.innerHTML = "You clicked me " + count + " times!";
}

使用闭包的代码如下:
将内层的嵌套函数所需的所有变量在外层函数中声明
从而将它们作为自由变量(外围变量)保存在环境中

window.onload = function() {
	var count = 0; 
	//现在,所有变量都是在一个函数中定义的,消除了名称冲突的问题
	var div = document.getElementById("message");

	var button = document.getElementById("clickme");
	//函数表达式创建闭包(函数有自由变量,因此创建了闭包)
	button.onclick = function() {
		count++;
		div.innerHTML = "You clicked me " + count + " times!";
	};
};
//加载事件处理程序window.onload执行完毕后,变量button就消失了,
//但button对应得按钮对象还在DOM中,其属性onclick存储了我们的闭包

//加载事件处理程序window.onload执行完毕后,变量count、div理应消失,
//但由于闭包作为值赋给了属性onclick,闭包(函数和环境)中的自由变量被保存下来
//而且无法直接访问,必须通过闭包函数访问(即:调用事件处理程序才能访问)

//这个闭包将一直存在,直到关闭网页。它已准备就绪,每当单击按钮时就会行动起来

闭包的环境中保存了自由变量count、div的值,需要时直接从环境中获取、访问并修改
(而不用再次从DOM中获取这个div对象,提高了效率)
(count也不再是全局变量,避免了名称冲突)

每当调用事件处理程序,闭包的环境中的count变量都会改变
每当调用事件处理程序(闭包函数),闭包的环境中的count变量都会因为执行了闭包函数中的代码count++而改变

可见,虽然完全可以不使用闭包,但闭包有很多优点

闭包的优缺点

优点:

  • 跨作用域访问变量:在函数外部也可以访问函数内的局部变量
  • 避免名称冲突:将自由变量保存在环境中,需要时直接从环境中获取,减少了全局变量,避免了名称冲突
  • 创建一个安全的环境:得到受保护的局部变量(私有性),它们不能直接访问
  • 提高代码执行速度:由于闭包函数的环境中保存了自由变量(未在本地定义),执行时遇到自由变量,直接从环境中获取它的值,而不用到外层查找
  • 让代码更简洁

缺点:内存消耗大,需要保存环境中的自由变量

注意 闭包包含的是实时的实际环境,而非环境的(静态的)副本

环境是实时更新的访问和改变环境中的自由变量(外围变量)值,要么直接改变环境中的自由变量值,要么改变(嵌套函数的)外层函数中的相应变量
换句话说,要访问和改变环境中的自由变量,有两种方法:

  1. 直接改变环境中的自由变量值:函数及其环境创建后,无法直接访问环境中的变量值,必须通过调用那个函数来访问和改变(如上面的Eg3所述)
  2. 改变(嵌套函数的)外层函数中的相应变量:在外层函数中,修改相应的自由变量的值

在闭包函数(嵌套函数)外面的外层函数代码中改变了变量,闭包函数的环境将引用改变后的新值

为证明这一点,有如下的示例

function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage

//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
	setTimeout(function() {
		alert(doneMessage);
	}, n);

	doneMessage = "OUCH!";
}

makeTimer("Cookies are done!", 1000);

输出"OUCH!"

下面是分析

function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage
...
}
makeTimer("Cookies are done!", 1000);

首先调用了makeTimer,并传入参数

function makeTimer(doneMessage, n) {
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
	setTimeout(function() {
		alert(doneMessage);
	}, n);
...
}

在这里,向setTimeout传递事件处理程序时,使用了函数表达式创建闭包
在此刻,环境中doneMessage的值为"Cookies are done!"

function makeTimer(doneMessage, n) {
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
	setTimeout(function() {
		alert(doneMessage);
	}, n);
	doneMessage = "OUCH!";
}

紧接着又在闭包外面修改doneMessage的值为"OUCH!"

最终,1000毫秒后调用事件处理程序时,由于闭包函数的环境中,doneMessage变量的值为"OUCH!",提示框中显示的是"OUCH!"

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
闭包在编程中有很多应用场景,以下是一些常见的例子: 1. 数据封装和私有变量:闭包可以用来创建私有变量,从而实现数据封装和保护。这在模块化编程和面向对象编程中非常有用。 2. 函数工厂:闭包可以用来创建一系列相关的函数,这些函数共享相同的外部变量。这在创建类似于Python中的装饰器或JavaScript中的高阶函数时非常有用。 3. 延迟执行和计时器:闭包可以用来实现延迟执行和定时器功能。例如,在JavaScript中,setTimeout和setInterval函数使用闭包来实现延迟执行和定时器功能。 4. 记忆化(Memoization):闭包可以用来实现记忆化,即缓存函数的计算结果,以便在后续调用中重用。这可以提高函数的性能,特别是在处理计算密集型任务时。 5. 事件处理和回调函数:在JavaScript等事件驱动的编程环境中,闭包常用于实现事件处理和回调函数闭包可以捕获事件处理函数的上下文,使得事件处理函数可以访问其所需的外部变量。 6. 部分应用(Partial Application)和柯里化(Currying):闭包可以用来实现部分应用和柯里化,这是一种将多参数函数转换为一系列单参数函数的技术。这可以简化函数调用,使得代码更加简洁和可读。 7. 实现迭代器和生成器:在某些编程语言中,例如Python和JavaScript闭包可以用来实现迭代器和生成器,这是一种用于遍历数据结构的高效方法。
最新发布
07-14

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值