网络编程
位置:首页>> 网络编程>> JavaScript>> 一文了解你不知道的JavaScript闭包篇

一文了解你不知道的JavaScript闭包篇

作者:霍格沃茨魔法师  发布时间:2024-02-23 11:37:36 

标签:JavaScript,闭包

前言

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

一文了解你不知道的JavaScript闭包篇

这是为什么?

首先解释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)
}

很酷是吧?块作用域和闭包联手便可&ldquo;天下无敌&rdquo;。

模块

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

0
投稿

猜你喜欢

手机版 网络编程 asp之家 www.aspxhome.com