作用域
动态作用域与词法作用域
1 | function foo() { |
如果在你看来,此处应该输出2(当然此处涉及到对于变量提升的理解),则说明你关注的是foo()
的声明;如果你认为会输出3,则你关注的是函数的调用。借此例引出词法作用域和动态作用域的主要区别:
词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定 的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
如何理解运行时确定这个概念,bar()
调用foo()
,动态生成调用栈,基于调用栈生成作用域链,而不是基于代码中的作用域嵌套生成作用域链(词法作用域的做法)。
对于变量的查找,js是遵循词法作用域的,并且现在的大多数语言都是遵循词法作用域的。为什么js不使用动态作用域?我觉得一方面是因为,静态的更好控制,动态的不好控制。javascript引擎会在在编译阶段进行数项性能的优化。其中一些优化依赖于能够根据代码的词法进行静态的分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。
1 | //作用域嵌套实例 |
块级作用域
函数作用域和全局作用域,都很好观察和理解。那什么是块级作用域?let 关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说let为其声明的变量隐式地了所在的块作用域。同样的,const声明也可以创建块级作用域。
1 | //隐式创建块作用域 |
推荐使用显示创建块作用域。在这个例子中,我们在 if 声明内部显式地创建了一个块,如果需要对其进行重 构,整个块都可以被方便地移动而不会对外部 if 声明的位置和语义产生任何影响。
变量提升
1 | //例1 |
按照结果逆推,可以感觉到变量被声明到了顶部。实际上也是:引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。(所以函数声明也能被提升)值得注意的是:第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。所以是undifinded。
闭包
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境。
定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
1 | function foo(){ |
技术上来讲,也许是。但根据前面的定义,确切地说并不是。我
认为最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却 是非常重要的一部分!)函数 bar() 具有一个涵盖 foo() 作用域的闭包。但是通过这种方式定义的闭包并不能直接进行观察,也无法明白在这个代码片段中闭包是 如何工作的。我们可以很容易地理解词法作用域,而闭包则隐藏在代码之后的神秘阴影 里,并不那么容易理解。
1 | function foo(){ |
函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作 一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实 际上只是通过不同的标识符引用调用了内部的函数 bar()。
bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃 圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很 自然地会考虑对其进行回收。上一个作用域嵌套实现的闭包,foo()的内部作用域会被销毁
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此 没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。