<>前言
闭包可以说是最最最最最最常见的面试题之一了,很多人面试的时候都被问到过闭包的知识,闭包的概念是什么?闭包的好处和坏处是什么?闭包有哪些应用场景了?
前两天朋友面试的时候也被问到过这个问题,网上的答案也有很多:有的说能够读取其他函数内部作用域的函数就是闭包,也有的说所有的函数都是闭包,哪怕是全局作用域上的,因为它能获取到全局作用域上的变量…答案也是很不统一,这就让还是新手的我对于答案有点举棋不定了,于是重温了一下《你不知道的javascript上卷》和《javascript高级程序第四版》,看了一下这两本书对于闭包的解释,记录一下相关知识,希望能够帮助到大家。
<>初始闭包
《你不知道的javascript》 javascript中闭包无处不在,你只需要能够识别并拥抱它。
闭包就是基于词法作用域的,你在书写代码的时候产生的一种现象。
当函数能够记住并访问自己所在的词法作用域的时候就产生了闭包
function foo(){ var a = 2; function bar(){ console.log(a) //2 } } foo()
上述的这段代码中,bar可以说也是一个闭包,因为它能够访问到他所在的作用域里面的变量。但是他却是封闭在foo里面的,就相当于一个词法作用域的查找规则而已,自己的作用域没找到,去上一个作用域找。也是闭包的一部分
function foo(){ var a = 2; function bar(){ console.log(a) } return bar } var
baz= foo() baz() //2 -----------这才是闭包的效果
我们将bar函数传递出去,然后调用foo函数的时候赋值给baz,调用baz就相当于调用了内部的bar函数了,而且bar相当于在自己所在的词法作用域之外执行了。foo函数执行之后,正常的话内部的作用域会被销毁,垃圾回收机制也会释放不再使用的内存空间,但是闭包可以阻止这种事情,让foo内部的作用域依然存在着。这样以供bar函数在后面的任何地方,任何时间引用
bar函数依然持有对这个作用域的引用,这个引用就叫闭包。
无论我们以何种的方式将bar这个函数传递出去 function foo() { var a = 2; function bar() { console.log
( a ); // 2 } baz( bar); } function baz(fn) { fn(); // 作为参数传递出去 这也是闭包 } var fn;
function foo() { var a = 2; function bar() { console.log( a ); } fn = baz; //
直接赋值给全局变量fn} function bar() { fn(); // 这也是闭包 } foo(); baz(); // 2
上面这几组代码,有将函数作为参数传递出去的,也有将函数赋值给一个全局变量的,都让这个函数可以在词法作用域之外调用了,都是闭包。包括我们平时使用的一些计时器,定时器,事件监听器这些使用了回调
函数,都是闭包的应用 function getName(name){ setTimeout( function timer() { console.log(
name); }, 1000 ); } getName('小明')
//将一个内部函数timer传递给setTimeout,timer具有涵盖getName的闭包,还有这对name的引用
<>for循环与闭包
我们经常使用for循环 for (var i=1; i<=5; i++) { setTimeout( function timer() {
console.log( i ); }, i*1000 ); }
上述这段代码中,正常情况下我们想打印 1,2,3,4,5,但是事实上却以每秒一次的频率打印了五次6,6是从哪来的那?这个循环的终止条件是 i
不在<=5,所以首次成立条件的时候 i 就是6,打印的就是循环结束时候的值?这是为什么?
虽然这个timer函数在每次迭代的时候都定义一次,相当于定义了五个,但是都封闭在一个共享的全局作用域下面,就相当于只有一个i,根据js中setTimeout的运行机制来看,也是正常的。
那我们得如何处理那?我们可以在每次迭代的时候都创建一个作用域,这个样的话就不是在一个全局作用域了。 for (var i=1; i<=5; i++) { (
function() { setTimeout( function timer() { console.log( i ); }, i*1000 ); })();
}
我们先尝试使用了一下自执行函数来创建作用域。
还是不行…
我们可以在每次迭代的时候创建一个块级作用域 for (let i=1; i<=5; i++) { setTimeout( function timer() {
console.log( i ); }, i*1000 ); }
我们使用到了es6 中的let定义变量,在每次迭代的时候都创建一个块级作用域,这样是可以的
<>闭包的应用场景
闭包有什么应用场景?比如定时器,回调函数,防抖函数....很多都是用到了闭包 我们还可以封装私有变量 (function() { // 私有变量和私有函数
let privateVariable = 10; function privateFunction() { return false; } // 构造函数
MyObject= function() {}; // 公有和特权方法 MyObject.prototype.publicMethod = function()
{ privateVariable++; return privateFunction(); }; })(); var obj = new MyObject()
obj.publicMethod() //false
比如上面这种,我们使用一个自执行函数封装私有变量,创建一个构造函数表达式,我们没有用var定义,说明是一个全局的,在构造函数的原型上创建了一个publicMethod函数方法,这个函数就可以访问这个函数的词法作用域,就是一个闭包
我们也可以使用模块模式创建,返回一个单例对象 function singleton() { // 私有变量和私有函数 let privateVariable
= 10; function privateFunction() { return false; } // 特权/公有方法和属性 return {
publicProperty: true, publicMethod() { privateVariable++; return privateFunction
(); } }; } var foo = singleton(); foo.publicMethod() //false
singleton函数返回了一个对象,相当于一个模块实例,这个对象里面的publicMethod函数具有涵盖模块实例内部作用域的闭包,singleton函数也可以是自执行函数
<>小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。
闭包也会出现也写问题,比如将把HTML元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁。或者是你把函数return出来后 他就给
window了所以一直存在内存中。会造成内存泄露。 function Handler() { let element =
document.getElementById('element'); let id = element.id; element.onclick = () =>
console.log(id); element = null;//
将element设置为null,解除对这个对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。}
参考资料:
《你不知道的javascript》上卷
《javascript高级程序设计第四版》