深入理解JavaScript闭包

原文链接:Understanding JavaScript: Closures

 

为什么要深入学习JavaScript

JavaScript是当今世界最流行的一种编程语言。它可以在浏览器运行,也可以在服务器端运行,可以在移动设备,桌面设备,甚至冰箱上面运行。当我们在下载异国风情的图片,当你在处理任何类型的web开发,你都会在某个时刻编写或者处理JavaScript。

许多的web开发者声称他们知道JavaScript,因为他们能够编写可运行的代码。JS是一个你可以一个月入门写代码,但是余生都要不停学习掌握的语言。如果没有出错,没人会抱怨为什么你需要了解更多。

然而我觉得需要更深入了解这门语言。几年前我使用AngularJS和Node开发APP,而且我对我的技能非常的自信。把方法调来调去,相信自己已经征服了JS。

所以当面试官让我解释一下什么是闭包的时候,我有点懵逼了。我意思是我有点了解。我知道它和回调有关,而我总是在使用回调(当时我并不知道Promise),但是,我找不到词语去解释它,以及它是怎样工作的。

一次失败的JavaScript面试在我的开发生涯中让我感到非常难受且很受教。从那时起,我花了一年半的时间去达到JS更高级别的段位,而现在是我与大家分享它的时候了。从JavaScript最常见的面试题开始:

什么是闭包?

毫无意义,闭包有各种各样的使用,我们也经常用到闭包。每次在给事件添加回调处理的时候,都会用到闭包。

我见过好几种关于闭包的一句话的解释,但点击最多的是Kyle Simpson给出的:


Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

当函数在它的词法范围外执行的时候,仍然能够访问、使用它的词法范围内的数据。


(词法范围是什么呢?词法范围有时候又叫做静态范围,是很多编程语言都使用的一种规定。它设置变量的功能范围,使得只能够在定义它的代码快中使用。此范围是在代码编译时期确定的。 这种方式声明的变量经常叫做*私有变量)

这个解释可能有点笼统,所以我们要一点一点的拆开它,看看它并不想是黑魔法那样。

本文不会详细讨论作用域(将会有文档单独讨论),但是知道作用域对于理解闭包是如何工作的是很有必要的。闭包实际上是包含变量和函数的代码的一部分。在JavaScript中,每一个函数都创建一个新的作用域,它的变量和传递的参数只能在它的作用域里面使用。

如果你在一个函数内部声明了一个变量,那么该变量在函数外部不可访问。但是我们可以在函数中定义函数,这些函数在函数内部作用域有效。现在这些特殊的嵌套函数可以访问他们父函数的变量。

这并没有什么特殊的,因为全局作用域定义的函数都能够访问它们自己的变量,但是这里也有点东西。这些切套函数可以访问它们父函数的作用域,但是本身不能从父函数外部调用,除非我们以某种方式暴露出来。

我们可以把这些内部函数暴露出来,这样就可以在全局作用域使用。现在我们可以随心所欲的使用这些内部函数。但是,我们假设这个暴露的内部函数引用它的函数作用域中的一个变量。 这会有问题吗? 不会,因为这就是一个闭包。

闭包是暴露出来的嵌套函数。

我不确定这是不是关于闭包的最好的定义,但是提现出了闭包的精髓。闭包是暴露出来的嵌套函数,所以我们可以在外部访问这些函数的父函数的作用域。现在你理解我们前面所说的词法范围了吗?

现在我们定义一个名为person的函数,有一个名为name的参数,此函数范围一个函数对象greet,现在我们知道,当我们调用这个暴露出来的函数greet可以访问person的参数。所以即便变量name不是在greet中声明的,但是greet可以访问到它,因为这是一个闭包。

function person() {
  var name = "beauty";
  
  function greet() {
    return "Hello " + name;
  }
  
  return greet;
}

你可以在很多时候用使用这种特性,在我不了解闭包之前,我没想过背后的黑魔法,所以我能够使用封装和模块。

哈,哈,...模块?封装? 那些突然出现了。

闭包带来的模块和封装

Modules and encapsulation with Closures

当我深陷Javascript的漩涡中后,我发现很多复杂的此货都可以通过实践来解释。模块和风中是最好的离职,我们可以使用相同的策略一个一个拆开理解,我们从封装开始。

封装是编程的基本原则之一。研究过OOP的人对这块很熟悉,对那些不熟悉OOP的人来说,这是一种隐藏机制,它允许我们将一些数据设置为私有的。我们通常不希望将函数所有内容暴露出来,而是希望将函数的大部分属性都设置为私有的,不能被随便访问。

这是闭包很强大的地方,我们可以使用闭包访问父作用域使得在外部被调用的时候,实现封装。父函数中可能有很多变量和函数,我们可以在外部暴露出来使得可以被广泛的使用。

通过闭包,我们可以给我们的函数定义一个公共API,而其他的内部数据保持私有。

 

现在我们需要通过多实践来使用封装。接下来就是拆解模块化了。

 

模块

在ES6里面我们可以使用import和export关键字实现基于文件的模块化,但是我们应该意识到这紧紧是语法糖而已。

这是一个相当简单的例子,用于演示如何将某些函数的数据保密。我们可以使用暴露出来的嵌套函数在其他地方使用这些私有的数据。

 

在这个稍微更现实的例子中,我们有一个返回订单对象的函数。 唯一暴露的函数是calculateTotal。 它有一个订单函数的闭包,可以使用变量并传递参数,隐藏了计算订单总额时的内部逻辑,且允许以后添加运费或者其他逻辑。

在将来添加运费或其他内容。

独特之处

JavaScript有它的独特之处,事实上,有些比较特殊的地方真的是够让人头疼,并且大晚上调试代码。然而当我们使用不当的话,也不会有啥大问题。

一下代码经常会出现在JavaScript面试中,我们猜猜它的输出是什么。

我们这里所做的就是从1到5循环,并设置超时时间,使得在特定时间后打印当前数字,这段代码会打印出1,2,3,4,5吗?

for (var i = 1;i <= 5;i ++) {
  setTimeout(function timer() {
      console.log(i)
  },i * 1000);
}

而且,让我们吃惊的是,最后打印出来的结果是5个6,如果没在setTimeout中执行,可能不会有疑问,因为log会立即执行,但是显然对操作排队是造成这个结果的原因。

我们期望每一个setTimeout调用接收到它自己的i变量的一份拷贝,但是实际上会从父作用域访问它。并且因为排队的原因,第一个log调用将会在排队后1秒执行,而当1秒过去后,循环早就执行完了,而这个时候i变量的值已经是6了。

知道原因后怎么解决呢?因为setTimeout在全局范围内查找i变量,导致不打印我们想要的结果,我们可以将setTimeout封装在函数中并传给循环的变量i,这样setTimeout函数将从它的父作用域而不是作用域访问i。

for (var i = 1;i <= 5;i ++) {
  (function(i){
    setTimeout(function timer() {
    console.log(i)
  },i * 1000)
  })(i);
}

我们使用立即执行函数表达式(IIFE Immediately Invoked Function Expression),并将参数i传给此函数。顾名思义,IIFE就是在定义后立即执行的函数,就像我们这种用法一样可以创建一个作用域。每个函数被调用的时候,都有它自己的变量副本,setTimeout执行的时候,能够访问到正确的i。所以,使用上面的例子,能够打印出我们想要的结果(1,2,3,4,5)。

 

关于闭包的思考

本文揭示了闭包的本质,但是关于闭包还有很多东西和大量案例需要去学习思考。如果你想更深入探讨闭包的强大用法,我强烈介意Kyle Simpson的书《Scope & Closures》

我希望本文能够提高您对JavaScript的理解,并帮助你更好的理解闭包。如果觉得还可以,可以给你的朋友分享这篇文章。

如果您对更多JS相关内容感兴趣,可以点击这里订阅我的更多资讯,或者可以查看这个系列中的其他文章。

转载于:https://my.oschina.net/mingshashan/blog/3083781

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值