原文链接:Understanding JavaScript: Closures
为什么要深入学习JavaScript
JavaScript是当今世界最流行的一种编程语言。它可以在浏览器运行,也可以在服务器端运行,可以在移动设备,桌面设备,甚至冰箱上面运行。当我们在下载异国风情的图片,当你在处理任何类型的web开发,你都会在某个时刻编写或者处理JavaScript。
许多的web开发者声称他们知道JavaScript,因为他们能够编写可运行的代码。JS是一个你可以一个月入门写代码,但是余生都要不停学习掌握的语言。如果没有出错,没人会抱怨为什么你需要了解更多。
然而我感觉我已经够深入了解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可以访问到它,因为这是一个闭包。
functionperson() {
varname="beauty";
functiongreet() {
return"Hello "+name;
}
returngreet;
}
你可以在很多时候用使用这种特性,在我不了解闭包之前,我没想过背后的黑魔法,所以我能够使用封装和模块。
哈,哈,...模块?封装? 那些突然出现了。
闭包带来的模块和封装
Modules and encapsulation with Closures
当我深陷Javascript的漩涡中后,我发现很多复杂的此货都可以通过实践来解释。模块和风中是最好的离职,我们可以使用相同的策略一个一个拆开理解,我们从封装开始。
封装是编程的基本原则之一。研究过OOP的人对这块很熟悉,对那些不熟悉OOP的人来说,这是一种隐藏机制,它允许我们将一些数据设置为私有的。我们通常不希望将函数所有内容暴露出来,而是希望将函数的大部分属性都设置为私有的,不能被随便访问。
这是闭包很强大的地方,我们可以使用闭包访问父作用域使得在外部被调用的时候,实现封装。父函数中可能有很多变量和函数,我们可以在外部暴露出来使得可以被广泛的使用。
通过闭包,我们可以给我们的函数定义一个公共API,而其他的内部数据保持私有。
现在我们需要通过多实践来使用封装。接下来就是拆解模块化了。
模块
在ES6里面我们可以使用import和export关键字实现基于文件的模块化,但是我们应该意识到这紧紧是语法糖而已。
这是一个相当简单的例子,用于演示如何将某些函数的数据保密。我们可以使用暴露出来的嵌套函数在其他地方使用这些私有的数据。
在这个稍微更现实的例子中,我们有一个返回订单对象的函数。 唯一暴露的函数是calculateTotal。 它有一个订单函数的闭包,可以使用变量并传递参数,隐藏了计算订单总额时的内部逻辑,且允许以后添加运费或者其他逻辑。
在将来添加运费或其他内容。
独特之处
JavaScript有它的独特之处,事实上,有些比较特殊的地方真的是够让人头疼,并且大晚上调试代码。然而当我们使用不当的话,也不会有啥大问题。
一下代码经常会出现在JavaScript面试中,我们猜猜它的输出是什么。
我们这里所做的就是从1到5循环,并设置超时时间,使得在特定时间后打印当前数字,这段代码会打印出1,2,3,4,5吗?
for(vari=1;i<=5;i++) {
setTimeout(functiontimer() {
console.log(i)
},i*1000);
}
而且,让我们吃惊的是,最后打印出来的结果是5个6,如果没在setTimeout中执行,可能不会有疑问,因为log会立即执行,但是显然对操作排队是造成这个结果的原因。
我们期望每一个setTimeout调用接收到它自己的i变量的一份拷贝,但是实际上会从父作用域访问它。并且因为排队的原因,第一个log调用将会在排队后1秒执行,而当1秒过去后,循环早就执行完了,而这个时候i变量的值已经是6了。
知道原因后怎么解决呢?因为setTimeout在全局范围内查找i变量,导致不打印我们想要的结果,我们可以将setTimeout封装在函数中并传给循环的变量i,这样setTimeout函数将从它的父作用域而不是作用域访问i。
for(vari=1;i<=5;i++) {
(function(i){
setTimeout(functiontimer() {
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相关内容感兴趣,可以点击这里订阅我的更多资讯,或者可以查看这个系列中的其他文章。