在学习JS的路上,无论是实际应用还是面试,闭包肯定是一个绕不过去的技术点。在我的学习生涯中,闭包的难度其实不大,但因其涉及的技术点和应用比较多,理解起
来总有一种“管中窥豹”未见其真身的感觉。读了廖雪峰大大的一篇博客,结合高程的闭包篇章和我自己的理解,给自己梳理一下:
一、什么是闭包?特点是什么
众所周知,一般来说由于JavaScript的作用域链限制一个对象只能调用自己链上的对象(不借助bind和call)
1、自有活动对象 2、全局变量对象
如果1和2都没有,则会报错
而闭包特殊之处在于,除了拥有正常的作用域链外,它还可以调用另一个特殊函数的活动对象:
1、自有活动对象 2、另一个函数活动对象 3、全局变量对象
一言以蔽之:闭包是一种可以访问其他函数活动对象的函数。
一个典型的闭包例子:(来自廖雪峰博客)
function lazy_sum(arr) { var sum = function () { return arr.reduce(function (x, y) { return x + y; }); } return sum; } var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
当我们传入一个数组作为参数调用lazy_sum函数后,与以往不同的是,lazy_sum函数返回的并不是一个具体值而是另一个函数sum。所以f是一个指向sum函数的指针,
当调用结束后,lazy_sum函数的作用域链被消毁,但是sum函数的作用域链环境并不会随之消失,而是随时存在于内存当中,等待调用。
必要时,我们可以用f()去调用这个函数,取得值为数值15。
这个sum函数就是一个闭包,它最大的特点是滞留,即不随调用函数的作用域链销毁而消失。
二、简单的功能,你不一定非要使用闭包
闭包的好处很多,但最大的坏处是:吃内存。因为上面提到的不消失特性,闭包函数非常占用内存,过度使用闭包可能造成内存占用过大。JS引擎可以使用回收垃圾机制清
理闭包,但还是谨慎为妙。
当你不需要一个闭包再服务时可以使用手动方法释放内存,以上面那个例子为例:
let f = lazy_sum([1,2,3,4,5]) let num = f() //使闭包指针指向null即可 f = null
三、闭包的经典应用
1、闭包计时器(阐述闭包语法)
function create_counter(initial) { var x = initial || 0; return { inc: function () { x += 1; return x; } } }
这次的闭包是create_counter函数的局部变量x。每次调用后,它接收最初传入的参数后,被当作闭包函数返回给调用者,即便是create_counter调用结束后也不会消失。
调用后就是一个程序之中不会断电的计时器,例如
var c1 = create_counter(); c1.inc(); // 1 c1.inc(); // 2 c1.inc(); // 3 var c2 = create_counter(10); c2.inc(); // 11 c2.inc(); // 12 c2.inc(); // 13
2、模拟面向对象
面向对象是一种思想而不是一种具体的技术。实际编程中需要用到面向对象的地方实在是太多。JS没有class机制创造对象,但闭包恰好是JS实现创造面向对象编程的技术。
function Person(){ let name = 'default'; return{ getName:function(){ return name; }, setName:function(newName){ name = newName }, } }; let p1 = Person() p1.setName('小王') alert(p1.getName()) // 小王 let p2 = Person() p2.setName('小张') alert(p2.getName()) // 小张
通过这段程序可以看到,每当一个Person函数被调用后,即创造了一个实例,有点像JAVA之中的new关键字创造出的对象,它们的属性功能相同,但并不再指向同一个对象,证据就是两次alert()展示的输出数据之中,分别存在了一个name变量保存了‘小王’和‘小张’,它们是独立存在的,真正在JS之中实现了面向对象编程。
在后续的程序之中,每当我们需要创造人物时候就可以let x = Person() 创造出一个新的Person实例,保存一个特有独立的对象供我们使用。为什么说一定要懂闭包,使用JS不会闭包相当于使用JAVA不会创建对象一样。
四、闭包对变量的取值,一个不能算BUG的BUG
在闭包中,闭包函数只能取得包含函数中任何变量的最后一个值:
function createFunctions(){ let resual = new Array(); for(let i =0;i<10;i++){ result[i] = function(){ return i; }; } return result; } let result = createFunctions() alert(result[0])...alert(result[9])
按照直觉,似乎result元素的值应该分别是:0,1,...,9
但实际上,每个元素都是10。
本质上,闭包函数在返回值时,并不是立即执行的,不是说每次执行到resual[i] = function(){return i }
的时候就返回脚标值,它是在等10个函数全部调用执行之后才开始赋值。此时闭包函数调用的变量i===10,所以它读取了这个值然后赋在每个元素之中。
记得,在闭包之中使用循环会吃大亏,除非你创造另一个匿名函数并立即调用执行它才会纠正这个BUG,这个方法称之为:闭包的闭包
function createFunctions(){ let resual = new Array(); for(let i =0;i<10;i++){ result[i] = function(n){ return function(){ return n; }; }(i); } return result; }
让我们理一下思路,这样写,一层闭包是一个参数为n的匿名函数,它在每次执行时会创造二层闭包:是一个返回一层闭包参数n的闭包函数。由于使用了匿名函数立即执行
的(i)语法,一层闭包执行后创造的二层闭包会立刻返回传入的参数n(同时也是脚标值),二层闭包把这个值最终传入数组元素之中,这样一来每个数组元素都拥有独立的数据了。
正是有了这个二层闭包才使得循环的数据i避免了被匿名函数返回的BUG。
五、闭包的误认
例如一个生成UI的匿名函数:
let datamodel = { table:[], tree:{}, }; (function)(dm){ for(let i=0;i<dm.table.rows;i++){ let row = dm.table.rows[i]; for(let j=0;j<row.cells;i++){ drawCell(i,j) } } })(datamodel)
它自动执行并无法被访问,但没有返回什么函数,所以并不算是闭包。
再如:
let person={ name:'anna', showName:function(){ alert(this.name) }, waitShowName:function(){ setTimeout(this.showName.bind(this),1000); } } person.waitShowName() //1s后显示 'anna'
很多人会把这样的调用看作闭包,看起来也确实是waitShowName函数包含了一个setTimeout函数。但是请再回顾一下闭包的定义:
闭包是一种可以访问其他函数活动对象的函数。
这个waitShowName函数指针只不过是person对象的一个方法,它能够调用showName完全是因为使用了this传递了调用对象person,因为它们同属person的活动对象。
并非因为它是闭包。也就是说
是依靠person.waitShowName()传来的this对象调用了showName方法,它本身完全不能调用person的任何属性,所以它不是一个闭包。
但如果改成这样:
function person(){ name = 'anna' showName:function(){ alert(name) }, return{ waitShowName:function(){ setTimeout(showName,1000); } } } person().waitShowName() //1s后显示 'anna'
此时的waitShowName()函数则是一个货真价实的闭包函数了,它调用了person对象之中的showName对象,真正的访问了其他函数的活动对象。
如果觉得有启发,请给我点个♥吧!!!