在es5中,js的知识点就那么几个,而闭包就是其中一个已经被说烂的知识点,但是每一次认真的回顾它,都会让我的理解更深一步。
曾经对于其它知识点也写过很多篇笔记,现在看来似乎理解的深度又不够了。同理,在此写下的这篇文章,可能在日后看来也只是冰山一角。
2019/12/05 更新
闭包是属于词法作用域范围的,因为将嵌套的函数返回,该函数并未被执行。所以在词法作用域的情况下,判断出函数引用了外部变量,这个函数和外部变量(词法作用域内的变量)形成了闭包。
私以为,闭包是一种结论的现象,而不是一个过程。换句话说,执行完某段代码产生了闭包,而不是在执行的过程中创建了闭包去完成了某件事情。
概念引用 MDN 的原文:
闭包是函数和声明该函数的词法环境的组合。
我觉得上面的表述还是不严谨的,例如:
const a = (x) => {
let b = 1
return () => {
console.log(x)
}
}
a(1)()
由概念此处的代码产生了闭包:匿名函数和声明该函数的词法环境,即
{
anonymous: Function,
b: undefined,
arguments: { // 形参在函数初始化时就已经被确立,而此时的变量对象还未被赋值
x: 1,
length: 1
}
this: undefined,
scope: [Context.a, Global Context]
}
而实际上,变量 b 是不存在的,也就是说这段代码产生的闭包,不应该包含 b,它包含了:匿名函数和形参 x。
与之相关的一个概念是词法作用域,也称为静态作用域,也就是说在词法分析的时候,就被确定的作用域。例如:
var x = 10
function getX() {
alert(x)
}
function foo() {
var x = 20
getX()
}
foo()
按照静态作用域来分析,结果是 10;而按照动态作用域来看,结果是 20。很显然,js 是前者,接下来我们按步骤分析下这段代码的创建与执行:
- 全局环境,编译时,创建词法作用域 getX.scope = Global Context; foo.scope = Global Context;
- 声明并初始化函数
function getX
、function foo
、 声明var x
并初始化为 undefined - 赋值 x = 10
- 执行 foo,将 foo 压入栈,生成执行上下文,初始化变量对象,确定 this = undefined
- 拷贝词法作用域,并将当前上下文与作用域结合初始化作用域链,
- 然后变量对象被赋值成为活动对象,调用 getX,在当前活动对象中不存在,根据作用域链向上查找,在 Global Context 中查找到定义
- 执行 getX,函数初始化过程同理,在执行变量 x 时,在当前活动对象中未找到,根据作用域链向上查找,所以是 10
理论上,在全局环境中,此时的 getX 和 x 就属于闭包。
但是词法环境的概念不同于我们讲的变量对象、作用域等,它分为:环境记录 和 外部引用。
但是在这里,我把它理解为:
- 环境记录是 变量对象 + this
- 外部引用是 作用域链
存在于闭包中的变量与声明都会一直保存在内存中,直到垃圾回收机制认为它可以被回收。如何有效的验证?
es6 中的 WeakSet / WeakMap 对于对象是弱引用,垃圾回收机制会忽视它的引用,可以用来作为评判标准吗?但是垃圾回收的时间是不可控的,反过来说,一个变量或函数的销毁并不等同于真的立马就被销毁了,只能等待垃圾回收机制后才能算是被真正销毁。
- MDN Closures 用来初步认识词法环境
- 词法环境和闭包 用来初步理解词法环境与闭包的关系
- 执行环境一些概念解释 词法环境的一些概念
- javascript中词法环境、领域、执行上下文以及作业详解 词法环境归纳性的理解