一文了解你不知道的JavaScript闭包篇
作者:霍格沃茨魔法师 发布时间:2024-02-23 11:37:36
前言
JavaScript语言中有一个非常重要又难以掌握,近似神话的概念-闭包。对于有一点JavaScript使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生。JavaScript中闭包无处不在,我们只需要能够识别并拥抱它。它是基于词法作用域书写代码时所产生的自然结果,在代码中随处可见。
理解闭包
下面用一些代码来解释这个定义:
function foo(){
var a = 2;
function bar(){
console.log(a);//2
}
bar();
}
foo()
这是闭包吗?也许是的,但似乎这种方式对必报的定义并不能直接进行观察,也无法明白这个代码片段中闭包是如何工作的。我们很容易地理解词法作用域,而闭包则隐藏在代码之后的神秘阴影里,并不那么容易理解。
下面我们来看一段代码,清晰的展示了闭包:
function foo(){
var a = 2;
function bar(){
console.log(a)
}
return bar;
}
var baz = foo();
baz() //2-------这就是闭包的效果。
函数bar的词法作用域能够访问foo()的內部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。在foo()执行后,它的返回值(bar函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数baz().foo內部的bar()显示可以被正常执行。
在foo()执行后,通常会期待foo()的整个內部作用域都被销毁,因为我们知道引擎有垃圾回收器来释放不再使用的空间。由于foo()似乎不会在被利用,所以大脑很自然的认为会对其进行回收。
而闭包的神奇之处正是可以阻止这件事的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个內部作用域呢?原来是bar()本身在使用。所以拜bar()所声明的位置所赐,它拥有覆盖foo()内部作用域的闭包,使得foo()的作用域能够一直存活,以供bar()在之后任何时间都可以被调用。
bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
当然,传递函数也是可以间接的:
var fn;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
fn = baz;
}
function bar(){
fn()
}
foo();
bar(); //2-------这就是闭包!
无论通过何种手段将內部函数传递到所在的词法作用域之外,它都会持有原始定义作用域的引用,无法在何处执行这个函数都会使用闭包。
升级版闭包
前面的代码片段可能有些死板,并且为了解释如何使用闭包而把代码写的很明显。但其实闭包在实际操作中是个很好玩的工具,而且大家也一定都用过闭包。现在让我们来搞懂这个事实:
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000);
}
wait ("Hello closure")
将一个内部函数(名为timer)传递给setTimeout().timer具有涵盖wait()作用域的闭包,因此还保有对变量message的引用。
wait(...)执行1000毫秒,它的內部作用域并不会消失,timer函数依然保有wait()作用域的闭包。这就是闭包。我不知道你在生活中都会写什么样的代码,但在定时器、事件 * 、Ajax请求、跨窗口通信或者任何其他的异步任务,只要你使用了回调函数,实际上就是在使用闭包。
var a = 2;
(
function IIFE(){
console.log(a)
}
)()
虽然这段代码可以正常工作,但严格来讲它并不是什么闭包,因为函数并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的。
循环和闭包
要说明闭包,循环for是最常见的例子。
for(var i = 1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}
正常情况下,我们对这段代码的行为预期是分别输出数字1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次6。
这是为什么?
首先解释6是从哪里来的。这个循环的终止条件是i不再小于等于5.条件首次成立时i的值是6.因此,输出显示的是循环结束时i的最终值。
其次要清楚,延迟函数的回调会在循环结束才执行。事实上,当给定时器运行时即使每个迭代器中执行的是setTimeout(..,0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6出来。
那为什么没有获得我们预期的结果呢,从1~5输出?
根据作用域原理,尽管循环中五个输出函数是被分别定义出来的,但是他们都被封闭在一个共享全局作用域下,实际上还是共享一个i。
那好,知道解决方法咯,那就不共享一个i,让每一次的值都相互独立,每次都获得对应的值。
for(var i = 1;i<=5;i++){
(
function(){
var j = i;
setTimeout(function timer(){
console.log(j)
},j*1000)
}
)()
}
这样,将每一次的i值都传给另一个变量,保证i的实时更新,就可以正常输出1~5了!
知道原因了,举一反三,那是不是也会有其他解决办法呢?
仔细思考我们前面的解决方案。我们使用IIFE函数(立即执行函数)每次迭代时都创建了一个新的作用域。换句话说,我们每次迭代都产生新的块作用域。那是不是可以声明块作用域避免变量共享的问题呢?
for(let i = 1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}
很酷是吧?块作用域和闭包联手便可“天下无敌”。
模块
function CoolModule(){
var something = "cool";
var another = [1,2,3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join("!"));
}
return {
doSomething:doSomething,
doAnother:doAnother
}
}
var foo = CoolModule();
foo.doSomething();//cool
foo.doAnother();//1!2!3
这个模式在JavaScript中被称为模块,最常见的实现模块模式的方法通常被称为模块暴露。首先,CoolModule()只是一个函数,必须要通过它来创建一个模块实例。如果不执行外部函数,內部作用域和闭包都无法被创建。
其次,CoolModule()返回一个用对象字面量语法{key:value,...}来表示的对象。这个返回的对象中含有对內部对象而不是內部数据变量的引用。我们保持內部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上模块的公共API。
doSomething()和doAnother()函数具有涵盖模块实例內部作用域的闭包(通过调用CoolModule()实现)。
简单描述一下,模块模式的闭包需要具备两个必要条件。
(1)必须有外部的封闭函数,该函数必须至少被调用一次。(每次调用都会创建一个新的模块实例)。
(2)封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
另外,模块也是普通的函数,因此可以接受参数:
function CoolModule(id){
function identify(){
console.log(id)
}
return {
identify:identify
}
}
var foo1 = CoolModule("foo1");
var foo2 = CoolModule("foo2");
foo1.identify();//"foo1"
foo2.identify();//"foo2"
小结
闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里,但实际上他只是一个标准,显然就是关于如何在函数作为值按需传递的此法环境中书写代码的。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。
如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。
模块主要有两个特征:
(1)为创建内部工作域而调用了一个包含函数。
(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数內部作用域的闭包。
来源:https://juejin.cn/post/7164368699858616351
猜你喜欢
- 今天我们将介绍处理大量数据时非常方便的工具。我不会只告诉您可能在手册中找到的一般信息,而是分享一些我发现的小技巧,例如tqdm与 multi
- 目录一、前言二、方法1、代码2、运行一、前言SpringBoot作为后端开发框架,有强大且方便的处理能力。但是作为一个结合数据分析+前台展示
- 本文介绍 SQL Server 2000 企业版的新功能 - 索引视图。讲解索引视图并讨论一些提高性能的具体方案。什么是索引视图?许多年来,
- 首先谈谈它们的共同点吧:它们本质上都是一种对资源的独占锁定,都是由并发引起(如果数据库只有一个session,就谈不上锁定)。接着着重谈谈它
- Python Dash开发Web应用的控件基础本文主要是通过Dash的Checklist组件,简单介绍使用Dash开发的Web应用展示效果如
- 这里我昨天碰到的问题就是执行一段根据变量tableName对不同的表进行字段状态的更改。由于服务器原因,我不能直接在数据访问层写SQL,所以
- Oracle的系统要求企业版:CPU最低PENTIUM200M推荐PENTIUMIII1G以上 内存最低128M推荐512M 硬盘空间系统盘
- 网页编程中,在与数据库打交道的时候我们经常会碰到乱码的经常。本文就将介绍一种ASP读取MySQL数据库出现乱码的解决办法。情景再现:使用My
- 此站:http://www.cbmland.com/ 的页面离开时的效果非常NB!佩服的很。一开始,我以为是用事件 onunload,试了一
- 比如代码 binfo = {'name':'jay','age':20,'pytho
- 有的时候需要用python处理二进制数据,比如,存取文件,socket操作时.这时候,可以使用python的struct模块来完成.可以用
- [mysql]replace的用法(替换某字段部分内容)[mysql]replace的用法1.replace into  
- 使用python进行图片处理,现在需要读出图片的任意一块区域,并将其转化为一维数组,方便后续卷积操作的使用。 下面使用两种方法进行处理:co
- 1、异常的传播当在函数中出现异常时,如果在函数中对异常进行了处理,则异常不会再继续传播。如果函数中没有对异常进行处理,则异常会继续向函数调用
- 笔者电脑系统是win7,同时安装了Python2.7和Python3.6,但是在通过命令行直接使用“pip install XXX”安装Py
- 每个PHP脚本都限制了执行时间,所以我们需要通过 set_time_limit 来设置一个脚本的执行时间为无限长;然后使用 flush()
- 1.Http连接基础Http协议承载了互联网上的主要流量,然而说到传输,还要回归到最基本的网络分层模型TCP/IP。TCP/IP是全球计算机
- 当项目里用npm安装了babel-cli之类的包时,webstorm在索引node_modules文件时,会卡死接近这个问题的办法是把nod
- 今天我发现这个结论是错误的。但是为了方便理解,我仍然不建议大家在不熟悉sql语句时,把里面的约束跟外面的约束混为一谈。从可读性方面来说,可以
- 大凡人世间的痛苦,多是因放不下有时候我常想,痛苦,该是时光刮给生命的一场飓风吧生活,本就是以这样一种特别的方式,掀起遮盖的一切,让你看到人生