飞道的博客

理解闭包

206人阅读  评论(0)

前言

闭包可以说是最最最最最最常见的面试题之一了,很多人面试的时候都被问到过闭包的知识,闭包的概念是什么?闭包的好处和坏处是什么?闭包有哪些应用场景了?
前两天朋友面试的时候也被问到过这个问题,网上的答案也有很多:有的说能够读取其他函数内部作用域的函数就是闭包,也有的说所有的函数都是闭包,哪怕是全局作用域上的,因为它能获取到全局作用域上的变量…答案也是很不统一,这就让还是新手的我对于答案有点举棋不定了,于是重温了一下《你不知道的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高级程序设计第四版》


转载:https://blog.csdn.net/qq_42736311/article/details/115573668
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场