前端日常一问:为何要使用闭包?请说一下闭包的原理和使用场景

闭包的由来

说的闭包,首先就要知道作用域和作用域链。

作用域
作用域是一个变量和函数的作用范围。
分为全局作用域和局部作用域,在ES6之前,是没有块级作用域概念的,只有函数作用域(个人认为私有作用域更符合)。
函数作用域都是相对独立的,外部是访问不到函数作用域中的变量的。
比如

function fn1() {
	var name = 'nini';
}
console.log(name)

此时,我们在外部是访问不到fn1中的name变量的。

作用域链
作用域链其实就是一个对象列表或者对象链。
在javascript中,每个函数都有自己的执行上下文环境。当我们要查找一个变量时,首先会从当前作用域开始查找,如果当前查找不到就到父级作用域查找,直至找到该变量。如果作用域顶端(作用域顶端是全局变量)也查找不到的话,会抛出异常ReferenceError(ReferenceError同作用域判别失败有关)。

何为闭包

闭包,通俗的来说,就是在当前作用域能访问到外部作用域中的对象。实际上,闭包的存在是为了保护私有变量不被污染,形成不销毁的栈内存,里面的私有变量等信息保存下来。
就像在上述中,要在外部访问到fn1中的name,就可以使用闭包来实现。

function fn1() {
	var name = 'nini';
	function getName() {
		console.log(name)
	}
	return getName
}
var getName = fn1()
getName()

MDN中对闭包的定义是:

闭包是指可以访问到自由变量的函数

那什么是自由变量呢?其实就是指既不是当前函数的参数也不是当前函数的内部变量的变量。
其实这是理论上的闭包,实践上的闭包要满足以下两点:

  1. 即使创建它的上下文已经销毁,它仍然存在;
  2. 代码中引用了自由变量;

闭包的原理

实际上是利用了函数作用域链的特性。
如果函数内部定义的函数引入了外部函数的活动对象,就会把它添加到函数的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
理解上述这句话的前提是理解执行上下文。
执行上下文有3个属性,this、变量对象VO(进入执行上下文之后是AO)、作用域链。而函数都有一个属性[[scope]],会保存所有父变量到其中,当函数激活进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

我们来具体分析一下执行过程:
image
f函数执行上下文维护了一个作用域链,依然可以读取到 checkscopeContext.AO 的值。当f 函数引用了 checkscopeContext.AO 中的值的时候,checkscopeContext已经被销毁了,但是checkscopeContext.AO 仍然活在内存中,依然可以通过 f 函数的作用域链找到它。

使用闭包的好处

  • 可作为私有成员存在
  • 可使变量长期保持在内存中
  • 避免全局污染

闭包的缺点

javascript中如果对象不再被引用,那么这些对象将会被JS引擎的垃圾回收器回收;反之,这些对象一直会保存在内存中。

  • 对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量
  • 使用不当会导致内存泄漏

闭包的应用场景

拿到正确的值(模仿块级作用域)

for(var i = 0; i < 6; i++) {
    (function(j){
        setTimeout(() => {
            console.log(j);
        }, j * 1000);
    })(i)
}

设置私有变量

let name = Symbol();
class Private {
	constructor(s) {
		this[name] = s
	}
	foo() {
		console.log(this[name])
	}
}

函数防抖

function debounce(fn, wait=50) {
	let timer;
	return function(...arguments) {
		if(timer){
			clearTimeout(timer);
		}
		timer = setTimeout(()=>{
			fn.apply(this, arguments);
		}, wait)
	}
}

参考

JavaScript深入之闭包
从作用域链谈闭包
闭包实际应用场景

posted @ 2021-08-23 16:53  卖萌实习生  阅读(1020)  评论(0)    收藏  举报