一文了解你不知道的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