作用域和闭包一直是 JS 中比较高级的内容,这篇文章打算以图的形式,让大家真正理解作用域和闭包的内容。
作用域就是一套变量访问规则,这些规则包括变量如何存储和访问,也就是规定了哪些变量可以被访问,哪些变量不可以被访问。
假设有如下代码:
function foo(a) {
if (typeof a === "number" || a instanceof Number) {
const b = a + 1;
console.log(b);
}
}
foo();我们来看下这段代码。 这段代码在 JS 引擎执行的时候究竟发生了什么? 这里我画了个图:
如上图,左边部分是编译器。 右半部分是作用域链。上述代码执行的具体过程大概是:
-
JS 源代码经过语法分析,转化成 tokens
-
tokens 经过语义分析,转化为 AST(抽象语法树)
-
抽象语法树会被转化为字节码
-
JS 运行时开始运行这段上面生成代码
-
当代码执行到函数声明的时候,引擎会向 scope chain 询问(一个 RHS)foo 是否已经声明 在这里是没有声明,会在当前 scope(也就是 global scope)创建一个 foo
console 是 内置对象, 虽然不是我们声明的,但是它已经在全局作用域了。
-
执行 foo。 引擎同样会询问 scope chain(一个 RHS),foo 是否已经声明。 在这里是声明了,如果没声明会报
Reference Error。 -
代码进到了 foo, 我们创建一个新的作用域,这个作用域指向全局作用域,从而形成作用域链。
-
下次引擎发送 RHS,如果当前作用域找到就返回,找不到就沿着链找,最终都找不到就报
Reference Error, 过程类似原型链。 -
剩下的我就不解释了,应该能看懂
注意图中块级作用域的位置,这也就解释了,为什么块级作用域声明的变量在块外面是无法访问的。
上述过程忽略了一些细节,比如生成 optimized AST 和 optimized byte code 的过程
闭包就是当一个函数即使是在它的词法作用域之外被调用时,也可以记住并访问它的词法作用域。
从理论上来说JS 中一切都是闭包,其原因就是 JS 和大多数编程语言一样采用的是静态作用域.
但是我们通常谈论的是可观察的闭包,什么是可观察? 我们来举个例子,这是一个可观察的闭包例子:
function foo(a) {
const c = "closures";
if (typeof a === "number" || a instanceof Number) {
const b = a + 1;
console.log(b);
}
return () => c;
}
const func = foo();
func(); // 'closures'上面的代码foo作用域的c在foo的外侧被访问到了,这就是一个可观察的闭包.
为什么呢? 这和上面的说的好像不太一样? 不,是一样的。我同样画了一个图:
说的直白一点就是foo返回的函数记住了ta被定义的时候的作用域,因此你可以访问到foo里面定义的变量。
看起来好像是突破了作用域的限制,我们称之为可观察的闭包。
当然没有被这个返回的函数引用的变量还是会被GC销毁

