JavaScript基础:闭包

134 阅读3分钟

概念

闭包是指有权访问另一个 函数作用域中的变量的函数。我们在进行koa-compose源码解读时,可以看到是对闭包的最好应用。

function (middleware) {
	if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  return function (context, next) {
  	let index = -1
  	return dispatch(0)
  	function dispatch (i) {
  		if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  		index = i
  		const fn = middleware[i]
  		if (i === middleware.length) fn = next
  		if (!fn) return Promise.resolve()
  		try {
  			return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
  		} catch(err) {
  			return Promise.reject(err)
  		}
  	}
  }
}

我们可以看到在koa-compose源码中,其返回的匿名函数就是闭包,匿名函数返回的dispatch函数也是闭包,都保持着对外层函数作用域中变量的引用。

原理

JavaScript采用的是词法作用域(lexical scoping),函数的执行依赖于变量作用域,而基于词法作用域函数变量作用域是在函数定义时决定的,而不是函数调用时决定的。也就是说根据代码的书写顺序我们就可以知道函数的作用域,当然也可以使用代码欺骗词法作用域,如使用eval函数、witch操作符,但是在代码书写时是完全不被推荐的方式,且会造成问题。

function compose {
	return function (context, next) {
		let index = -1;
		return dispatch(0);
		function dispatch (i) {
		}
	}
}

我们把koa-compose不必要的代码删除掉,只保留闭包需要的代码,我们可以看到书写时的嵌套关系就是很明确的作用域范围。其中我们知道函数作用域访问外部作用域的变量是通过作用域链。函数在执行时会创建一个执行环境execution context及相应的作用域链。 然后,使用arguments和其他命名参数的值来初始化函数的活动对象activation object。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直至作为作用域链终点的全局执行环境。变量的读取和写入就是通过作用域链。因此,dispatch函数执行时,作用域链上有匿名函数的变量对象也有compose函数的变量对象,所以其可以对外层函数定义的变量和参数对象进行访问,比如使用middlewarecontextindex等。

作用域链
函数执行环境都有一个变量对象variable objext。全局环境的变量对象始终存在,而像compose函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在函数执行时会会通过作用域链到各个变量对象直至全局变量对象,这个作用域链被保存在内部的[[Scope]]属性中。 作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲, 当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局执行环境的变量对象。 但闭包的又有所不同,闭包在执行时其外部函数作用域被链到其作用域链上,其被返回时保持着对外部变量对象的引用,因此外部变量对象并不会在外部函数执行完毕时被垃圾回收。因此,这也就是闭包可以访问外部执行环境的变量的秘密所在。