当前位置: 首页 > 科技观察

探索JavaScript中的闭包

时间:2023-03-15 22:25:51 科技观察

闭包令人困惑,因为它是一个“无形”的概念。在处理对象、变量或函数时,您会想,“我需要一个变量”,并将其添加到您的代码中。闭包有多种不同的形式。当很多人注意到闭包时,他们已经不知不觉地多次使用它们——您可能也是。所以学习闭包并不是要理解任何“新”概念,而是要理解你“已经”接触过的东西。太久不见。当“函数访问在其外部定义的变量”时,您需要闭包。例如,这段代码包含一个闭包:letusers=['Alice','Dan','Jessica'];letquery='A';letuser=users.filter(user=>user.startsWith(query));注意user=>user.startsWith(query)本身就是一个函数。它使用查询变量。但是,查询变量是在函数“外部”定义的。那就是闭包。“如果你愿意,你可以在这里停止阅读。”本文的其余部分以不同的方式对待闭包,而不是解释闭包是什么,而是带您完成“发现”它的过程——就像1960年代的第一批程序员所做的那样。第1步:函数可以访问外部变量要理解闭包,我们需要了解一些变量和函数。在这个例子中,我们在eat函数中声明了food变量。functioneat(){letfood='cheese';console.log(food+'isgood');}eat();//=>'cheeseisgood'但是如果我们稍后想改变eat函数的食物变量怎么办?对于So,我们可以将food变量本身移出函数,移到顶层:letfood='cheese';//我们移到functioneat(){console.log(food+'isgood');}之外,这样我们就可以需要时“从外面”修改食物:eat();//=>'cheeseisgood'food='pizza';eat();//=>'pizzaisgood'food='sushi';eat();//=>'sushiisgood'换句话说,食物变量不再是eat函数的本地变量,但eat函数仍然可以轻松访问它。“函数可以访问它们之外的变量”。停下来想一想,确保你对这个想法没有任何怀疑。然后进行第二步。第2步:将代码包装在函数调用中假设我们有一些代码:/*一些代码片段*/这段代码的作用无关紧要。但是假设“我们要运行它两次”。一种方法是复制和粘贴:/*somecodesnippet*//*somecodesnippet*/另一种方法是使用循环:for(leti=0;i<2;i++){/*somecodesnippet*/第三种方法,也是我们今天特别感兴趣的方法,将其包装在一个函数中:functiondoTheThing(){/*somecodesnippet*/}doTheThing();做事();该函数给了我们很大的灵活性,因为我们可以在程序的任何一点执行这个函数任意次数。如果需要,“我们也可以只调用一次”:functiondoTheThing(){/*一些代码片段*/}doTheThing();请注意,上面的代码相当于原始代码片段:/*somecodesnippet*/换句话说,“如果我们有一段代码,将代码“包装”成一个函数,并且只调用它一次,那么我们不要改变代码的作用”。我们将忽略此规则的一些例外情况,但总的来说这应该是有道理的。仔细思考这个想法,直到你的大脑完全理解它。第3步:发现闭包之前我们探索了两种不同的想法:函数可以访问在它们之外定义的变量。将代码包装在函数中并调用一次不会改变结果。那么当你把它们结合起来会发生什么。我们将从第1步的代码开始:letfood='cheese';functioneat(){console.log(food+'isgood');}eat();然后,将整个示例中的代码包装到一个函数中,该函数将被调用一次:functionliveADay(){letfood='cheese';functioneat(){console.log(food+'isgood');}eat();}liveADay();再次查看这两个代码片段并确保它们是等价的。此代码有效!但是仔细看,会发现eat函数在liveADay函数内部。这是允许的吗?我们真的可以将一个函数放在另一个函数中吗?在某些语言中,这样写的代码是“无效的”。例如这种代码在C语言中是无效的(没有闭包)。这意味着在C语言中,上面的第二个结论是不正确的——我们不能只是将一些代码包装在一个函数中。但JavaScript不受此限制。再看看这段代码,注意在哪里声明和使用食物:`}eat();}liveADay();让我们逐步浏览这段代码。首先在顶层声明liveADay函数,然后立即调用它。它有一个food局部变量,还包含一个eat函数。然后调用eat函数。因为eat在liveADay内部,所以它“看到”所有变量。这就是它可以读取食物变量的原因。“这是一个关闭”。“当函数(例如eat)读取或写入在其外部声明的变量(例如food)(例如在food中)时,我们说存在闭包。”花点时间多读几遍,确保你理解上面的代码代码。下面是本文开头介绍的例子:letusers=['Alice','Dan','Jessica'];letquery='A';letuser=users.filter(user=>user.startsWith(query));如果用函数表达式重写的话,更容易注意到闭包:letusers=['Alice','Dan','Jessica'];//1。查询变量声明在外面letquery='A';letuser=users。filter(function(user){//2.我们在嵌套函数中//3.然后我们读取查询变量(在外部声明!)returnuser.startsWith(query);});每当在函数外部访问函数时,当我们声明一个变量时,我们就说它是一个闭包。该术语本身的使用有些松散。在这个例子中,有些人将“嵌套函数本身”称为“闭包”。其他人可能将访问外部变量的“技术”称为闭包。其实没关系。函数调用的幽灵闭包看起来很简单,但这并不意味着它们没有自己的陷阱。如果你真的仔细想想,函数可以在外部读写变量这一事实具有深远的意义。这意味着只要可以调用嵌套函数,这些变量就会“存活”:5000);}liveADay();在这里,food是liveADay()函数调用中的局部变量。在我们退出liveADay之后,很容易认为它“消失了”并且不会再回来困扰我们。然而,在liveADay中,我们告诉浏览器在五秒内调用eat。然后,eat读取food变量。“因此,JavaScript引擎需要在特定的liveADay()调用中保持食物变量可用,直到调用eat为止。”从这个意义上说,我们可以把闭包看作是过去函数调用的“幽灵”或“记忆”。即使我们的liveADay()函数调用早已完成,但只要嵌套的eat函数仍然可以调用,它的变量就必须继续存在。幸运的是,JavaScript为我们做了这些,所以我们不必再考虑它了。为什么会有“关闭”?最后,您可能想知道为什么以这种方式调用闭包。主要是历史原因。熟悉计算机科学术语的人可能会说像user=>user.startsWith(query)这样的表达式具有“开放绑定”。换句话说,从这个用户是什么(一个参数)就很清楚了,但是还不能确定查询是孤立的。当我们说“实际上,查询指的是在外部声明的变量”时,我们正在“关闭”一个开放绑定。换句话说,我们得到了一个闭包。并不是所有的语言都实现闭包。比如像C这样的一些语言,是根本不允许嵌套函数的。因此,一个函数只能访问它自己的局部变量或全局变量,而不能访问父函数的局部变量。当然,这种限制是痛苦的。还有像Rust这样的语言,它们实现了闭包,但是闭包和常规函数的语法是分开的。因此,如果你想从函数外部读取一个变量,你必须选择在Rust中使用那个变量。这是因为在幕后,闭包可能需要引擎在函数调用之后保留外部变量(称为“环境”)。这种开销在JavaScript中是可以接受的,但对于非常低级的语言,它可能会导致性能问题。至此,希望你对闭包的概念有了深刻的理解!