重学JavaScript(函数)闭包
前言
我们知道,作用域链查找标识符的顺序是从当前作用域开始一级一级往上查找。因此,通过作用域链,JavaScript函数内部可以读取函数外部的变,但反过来,函数的外部通常则无法读取函数内部的变量。在实际应用中,有时需要真正在函数外部访问函数内部的局部变量,此时最常用的方法就是使用闭包。
那么什么是闭包?所谓闭包,就是同时含有对函数对象以及作用域对象引用的对象。闭包主要是用来获取作用域链或原型链上的变量或值。创建闭包最常见的方式是在一个函数中声明内部函数(也称嵌套函数),并返回内部函数。此时在函数外部就可以通过调用函数得到内部函数。虽然按照闭包的概念,所有访问了外部变量的JavaScript函数都是闭包。但我们平常绝大部分时候所谓的闭包其实指的就是内部函数闭包。
闭包可以将一些数据封装私有属性以确保这些变量的安全访问,这个功能给应用带来了极大的好处。需要注意的是,闭包如果使用不当,也会带来一些意想不到的问题。下面就通过几个示例来演示一下闭包的创建、使用和可能存在的问题及其解决方法。
示例1: 创建闭包。
<!DOCTYPE html> <html> <head> <title>闭包</title> </head> <body> <script type="text/javascript"> function outer(argument) { var b=0; return function inner (){ b++; console.log("内部的b:"+b); } } var func = outer();//1 通过外部变量引用函数返回的内部函数 console.log(func);//2 输出内部函数定义代码 func();//3 通过闭包访问局部变量b,此时b=1; console.log("外部函数中b:"+b); //4 出错,报引用错误。 </script> </body> </html>
上述代码在外部函数outer中声明内部函数inner,并返回内部函数,同时在outer函数外面,变量func引用了outer函数返回的内部函数,所以内部函数inner是一个闭包。该闭包访问了外部函数的局部变量b。1处代码通过调用外部函数返回内部函数并赋给外部变量func,使func变量引用内部函数,所以2处代码将输出inner函数的整个定义代码。3处代码通过对外部变量func添加一对小括号后调用内部函数inner,从而达到在函数外部访问局部变量b的目的。执行4处的代码时将报ReferenceError错误,因为b是局部变量,不能在函数外部直接访问局部变量。
我们知道函数执行完毕时,运行期上下文会被销毁,与之关联的活动对象也会随之销毁,因此离开函数后,属于活动对象的局部变量将不能被访问。但是为什么上述示例中的outer函数执行完后,它的局部变量还能被内部函数访问呢?这个问题我们可以用作用域链来解释。
当执行1处代码调用outer函数时,JavaScript引擎会创建outer函数执行上下文的作用域链,这个作用域链包含了outer函数执行时的活动对象,同时JavaScript引擎也会创建一个闭包,而闭包因为需要访问outer函数的局部变量,因而其作用链也会引用outer的活动对象。这样,当outer函数执行完后,它的作用域对象因为有闭包的引用而依然存在,固而可以提供给闭包访问。
上述示例中的内部函数虽然有名称,但在调用是并没有用到这个名称,所以内部函数的名称可以缺省,即可以将内部函数修改为匿名函数,从而简化代码。
示例2: 经典闭包问题
<!DOCTYPE html> <html> <head> <title>经典闭包问题</title> <script type="text/javascript"> window.onload=function () { var abtn = document.getElementsByTagName("button"); for (var i = 0; i<abtn.length; i++) { abtn[i].onclick=function(){ alert("按钮"+(i+1)); } } } </script> </head> <body> <button>按钮1</button> <button>按钮2</button> <button>按钮3</button> </body> </html>
该示例期望实现的功能是,单击每个按钮时,在弹出的警告对话框中显示相应的标签内容,即单击3个按钮时将分别显示“按钮1”、“按钮2”、“按钮3”。
上述示例页面加载完后触发窗口加载事件,从而执行外层匿名函数,外层匿名函数执行完循环语句后使活动对象中的局部变量i的值修改为3。外层匿名函数执行完后撤销,但由于其活动对象中的abtn和i变量被内层匿名函数引用,因而外层匿名函数的活动对象仍然存在堆中供内层匿名函数访问。每执行一次循环都将创建一个闭包,这些闭包都引用了外层匿名函数的活动对象,因而访问变量i时都得到3,这样最后的结果是单击每个按钮,在警告对话框中显示的文字都是“按钮4” (i+1=3+1),与期望的功能不一致。造成这个问题的原因是,每个闭包都引用一个变量,如果我们使不同的闭包引用不同的变量,就可以实现输出的结果不一样。这个需求可使用多种方法实现,在此介绍使用立即调用函数表达式(IIFE)和ES6中的let创建块即变量的方法。
IIFE指的是:在定义函数的时候直接执行,即此时函数定义变成了一个函数调用的语句。要让一个函数定义语句变成函数调用语句,就需要将定义语句变为一个函数表达式,然后在该表达式后面再加一对圆括号()即可。将函数定义语句变为一个函数表达式的最常用方法就是将整个定义语句放在一对圆括号中。
1、IIFE中的函数为一个匿名函数
(function(name){ console.log("hello,"+name); })("maomin");
JS引擎执行上述代码时,会调用匿名,同时将后面圆括号中的参数maomin传给name虚参,结果得到:"hello,maomin"。
2、IIFE中的函数为一个有名函数
(function func (name) { console.log("I am"+name); })("maomin")
上述代码跟匿名函数完全一样。
示例3: 使用立即调用函数表达式解决经典闭包问题
<!DOCTYPE html> <html> <head> <title>使用立即调用表达式解决经典闭包问题</title> <script type="text/javascript"> window.onload=function () { var abtn = document.getElementsByTagName("button"); for (var i = 0; i<abtn.length; i++) { (function(num){ abtn[num].onclick=function(){ alert("按钮"+(num+1)); } })(i) } } </script> </head> <body> <button>按钮1</button> <button>按钮2</button> <button>按钮3</button> </body> </html>
上述代码中第二个匿名函数为IIFE,每次调用该匿名函数时将生成一个对应该函数的活动对象。该对象中包含可一个函数参数,值为当次循环的循环变量值。上述示例中,IIFE共执行了3次,因而共生成了3个活动对象,活动对象中包含的参数值分别为0、1和2,依次对应IIFE的3次执行。
每次执行IIFE时,将会产生一个闭包,该闭包会引用对应按钮索引顺序执行IIFE的活动对象,而闭包引用的活动对象中的参数值刚好等于按钮的索引值,因而单击3个按钮将在弹出的警告框中分别显示"按钮1"、“按钮2”、“按钮3”。
示例4:使用ES6中的let关键字创建块级变量解决经典闭包问题