js 回顾之前写的js闭包文章
很久以前,我曾经参考阮一峰dalao的一篇闭包科普,写了一篇文章(讲述我对js闭包的理解)。最近,我在知乎上发现了这个问题《阮一峰关于Javascript中闭包的解读是否正确?》,所以打算参考这个问题,更正一下之前的说法。
参考:https://www.zhihu.com/question/27712980
本文是对很久以前一篇文章的更正:
http://www.xie4ever.com/2016/11/22/js-%E7%A0%94%E7%A9%B6%E4%B8%8Bjs%E7%9A%84%E9%97%AD%E5%8C%85/
一、一些问题
(1)函数内部声明变量的问题
函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
这个说法是有问题的。“在函数内部声明变量不使用var”不等于“声明了一个全局变量”,而是“根本没有声明变量”,相当于访问了window的属性。
(或许称为“设置了window的属性比较好?”,似乎也不能这么说,window的属性应该是固定的,不能自己设置,也许可以理解为“给window的属性中加了一个变量”,即“访问window的属性所创建的变量”)。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>6.22闭包测试</title> </head> <body> <script> (function(){c = "hi"}()); console.log(window.c) </script> </body> </html> |
在function中定义了一个c(这里的c就是window的属性),随后就可以访问这个属性。
“全局变量”和“访问window的属性所创建的变量”存在区别,“访问window的属性所创建的变量”是可以delete的(相当于删掉了window属性中的某个变量),而“全局变量”是不能delete的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>6.22闭包测试</title> </head> <body> <script> a = "hi"; delete a; console.log(a) // not defined (function(){b = "hi"}()); delete b; console.log(b); // not defined var c = "hi"; delete c; console.log(c); </script> </body> </html> |
(2)不严谨的闭包说明
闭包就是能够读取其他函数内部变量的函数。
这个说法不严谨,“其他函数”不能是“任意的其它函数”(但是我个人理解尚浅,举不出来反例,有点难受)。
查看一下wikipedia对闭包的定义:
闭包是由函式和与其相关的参照环境组合而成的实体。
这个定义指明了闭包的组成部分,又限定的闭包的作用范围,是比较严谨的说法。
况且,闭包只需要有扩充变量的作用范围即可,不需要有“其他函数的存在”(我没理解这句话是什么意思)。
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。
即便在函数定义了函数,如果没有引用父函数作用域内的变量,照样不是闭包。
(3)例子有点复杂
阮一峰大佬出了这样一个思考题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>6.22闭包测试</title> </head> <body> <script> var name = "The Window"; var object = { name: "My Object", getNameFunc: function () { return function () { return this.name; }; } }; console.log(object.getNameFunc()()) </script> </body> </html> |
运行结果为:
1 |
The Window |
阮一峰dalao完全没有解释this和运行时机制,所以我们自己尝试理解这段程序。
首先看到object.getNameFunc(),调用了object对象中的getNameFunc()方法。这个方法的返回值是另一个方法本身。我们可以改写一下:
1 2 |
console.log(object.getNameFunc()) console.log(object.getNameFunc()()) |
运行结果变成了:
1 2 3 4 |
function () { return this.name; } The Window |
可以看见object.getNameFunc()就是另一个方法。为了执行这个方法,我们又加上了“()”,于是object.getNameFunc()()意为“执行方法中return的方法”。
那么问题来了,这个方法的内容为return this.name,这里的name指的是object类中定义的变量name,还是全局变量中的name?
要搞明白的是这里的“this”到底指向谁。对于this这个知识点,可以参考:
要清楚:“this跟函数在哪里定义没有半毛钱关系,函数在哪里调用才决定了this到底引用的是啥。”、“函数在哪里调用决定了this指向的是啥”。
我们从最初的调用看起:
1 |
console.log(object.getNameFunc()()) |
上面也提到了,object.getNameFunc()只是负责获取一个方法,我们称之为fun方法:
1 2 3 |
return fun = function(){ return this.name; } |
获取了fun方法之后,object.getNameFunc()就可以替换成fun,变成:
1 |
console.log(fun()) |
这里就是关键了,fun方法很明显是直接调用的,等价于:
1 |
console.log(window.fun()) |
fun方法中的this指向window,所以this.name指向全局变量中的name。
题外话,如何返回object对象中的name呢?
我们要牢记“谁调用指向谁”。既然要返回object对象中的name,那么this当然要指向object对象,也就是说必须得由object亲自调用某个方法返回this.name。
这个例子不适合使用闭包,我们稍微改写一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>6.22闭包测试</title> </head> <body> <script> var name = "The Window"; var object = { name: "My Object", getNameFunc: function () { return this.name; } }; console.log(object.getNameFunc()) </script> </body> </html> |
这里的getNameFunc方法是由object对象亲自调用的,所以this指向object对象,this.name指向objcet对象中的name变量。
二、回顾闭包
举个例子(因为a方法调用了另一个方法,并且引用了a方法中的变量,所以这是一个典型的闭包):
1 2 3 4 5 6 |
var a = function () { var test = {}; setTimeout(function () { console.log(test); }, 1000); } |
在上面的例子中,变量test在方法a中定义,但使在setTimeout方法的参数中对它保持了引用。
从理论上来说,当方法a执行完后(方法a执行完毕),方法a在执行过程中产生的变量、对象都可以被销毁。但是,因为test已经被引用,所以不能随这个方法a的执行结束而被销毁(我还没用完呢,你销毁了我怎么办),直到定时器里的方法(引用test的方法)被执行完毕。
再举个例子(因为obj方法的返回值是个对象,对象中有set和get两个方法,都引用了obj方法中的变量a,所以这是个稍微复杂一点的闭包):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var obj = function () { var a = ''; return { set: function (val) { a = val; }, get: function () { return a; } } }; var b = obj(); b.set('new val'); b.get(); |
在上面的例子中,从理论上来说,obj方法在执行完后,函数体内东西都应该被回收。但是,因为obj方法执行后的返回值b对象具有set和get方法,而这两个方法对a保持了引用,所以obj执行过程中产生的a就不能销毁。
直到set和get方法执行完毕,b对象被回收后,obj方法中的a才会被回收。
参照上面的两个例子,我们可以这样理解闭包:
闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
三、总结
有种突然开窍的感觉!