作用域
在进入作用域的讨论之前先明什么是作用域,通过一句话简单描述就是:一组明确定义的规则,它定义如何在某些位置存储变量,以及如何在稍后找到这些变量
词法作用域
作用域的工作方式有两种占统治地位的模型。其中的第一种是最最常见,在绝大多数的编程语言中被使用的。它称为 词法作用域,我们将深入检视它。另一种仍然被一些语言(比如 Bash 脚本,Perl 中的一些模式,等等)使用的模型,称为 动态作用域。
JavaScript采用的是词法作用域,这意味着作用域是编写时确定的,而不是运行时确定的,当然也可以通过欺骗来达到动态作用域,例如使用:eval
、with
等关键词
词法分析
JavaScript的代码运行,并不是像你想象的逐行编译,而是在进行编译前会进行词法分析。也就形成了我们所说的词法作用域。
可以通过下面的🌰来看:
1 | var value = 1; |
大多数人看到这个🌰第一个反应结果是:输出2
,但是需要注意的是foo的作用域并不存在与bar函数
中,因为JavaScript的作用域
是词法作用域,所以并不能获取到bar函数中 var value = 2
的声明。
下面我们来简单描述一下这段代码的执行过程:
- 在全局环境下声明了
value变量
、foo函数
、bar函数
- 执行
bar函数
, - 在函数
bar
内部声明了value变量
并赋值为2 - 执行,
foo函数
- 在函数内寻找
value变量
声明,未找到,向上一层作用域继续寻找 - 在顶层作用域
window
下寻找到了value
变量,若直到顶层作用域任未找到则报错 - 输出结果
1
动态作用域
上面已经描述了词法作用域的工作方式,这里我们来稍微讲讲与词法作用域完全对立的动态作用域
我们这里就以bash为例:
1 | value=1 |
我们将上面代码保存为scope.bash
的文件,通过执行bash scope.bash
,最终输出1
变量提升
在代码执行前,引擎会在执行前编译它。编译过程的一部分就是找到所有的声明,并将它们关联在合适的作用域上
例如这段代码:
1 | a = 2; |
当你看到 var a = 2;
时,你可能认为这是一个语句。但是 JavaScript 实际上认为这是两个语句:var a;
和 a = 2;
- 第一个语句,声明,是在编译阶段被处理的。
- 第二个语句,赋值,为了执行阶段而留在 原处。
于是可以认为代码被处理成这样了:
1 | var a; |
关于这种处理的一个有些隐喻的考虑方式是,变量和函数声明被从它们在代码流中出现的位置“移动”到代码的顶端。这就产生了“提升”这个名字
需要注意的是:提升是 以作用域为单位的
函数优先
函数声明和变量声明都会被提升。但一个微妙的细节(可以 在拥有多个“重复的”声明的代码中出现)是,函数会首先被提升,然后才是变量。
1 | foo(); // 1 |
将会被转变成:
1 | function foo() { |
注意那个 var foo
是一个重复(因此被无视)的声明,即便它出现在 function foo()...
声明之前,因为函数声明是在普通变量之前被提升的。
虽然多个/重复的 var声明实质上是被忽略的,但是后续的函数声明确实会覆盖前一个。
1 | function foo() { |
实际上转变成了:
1 | function foo () { |
闭包
闭包对于大多数熟练的JavaScript也算是一个模糊不清的概念,什么是闭包呢,闭包能给我们带来什么好处和坏处?
简单来说可以用一句话概括闭包的特性与作用:闭包就是函数能够记住并访问它的词法作用域,即使这个函数在它的词法作用域外执行
让我们跳进代码来说明这个定义:
1 | function foo() { |
上面的代码段被认为是函数 bar() 在函数 foo() 的作用域上有一个 闭包.换一种略有不同的说法是,bar() 闭住了 foo() 的作用域。为什么?因为 bar() 嵌套地出现在 foo() 内部。就这么简单。
根据文章上面的作用域
我们知道,函数的作用域是编写时定义的而不是运行时决定的,所以我们通过函数内部返回函数时,返回出来的函数的作用域链的起始位置依然是那个函数内部。由于在函数外部对函数内部值存在引用的关系,垃圾回收机制并不会将变量回收而是会一直在函数内部引用。
闭包的特性
根据闭包的定义我们能很容易记住其两大特点:
1、能够记住并访问它的词法作用域
2、即使在它的作用域外执行
1 | function foo() { |
bar()
被执行了,必然的。但是在这个例子中,它是在它被声明的词法作用域 外部 被执行的。foo()
被执行之后,一般说来我们会期望foo()
的整个内部作用域都将消失,因为我们知道 引擎 启用了 垃圾回收器 在内存不再被使用时来回收它们。因为很显然foo()
的内容不再被使用了,所以看起来它们很自然地应该被认为是 消失了。- 但是闭包的“魔法”不会让这发生。内部的作用域实际上 依然 “在使用”,因此将不会消失。谁在使用它?函数
bar()
本身。 - 有赖于它被声明的位置,
bar()
拥有一个词法作用域闭包覆盖着foo()
的内部作用域,闭包为了能使bar()
在以后任意的时刻可以引用这个作用域而保持它的存在。 bar()
依然拥有对那个作用域的引用,而这个引用称为闭包。
闭包使用场景
无处不在的闭包
1 | function wait(message) { |
- 我们拿来一个内部函数(名为 timer)将它传递给 setTimeout(..)。但是 timer 拥有覆盖 wait(..) 的作用域的闭包,实际上保持并使用着对变量 message 的引用。
- 在我们执行 wait(..) 一千毫秒之后,要不是内部函数 timer 依然拥有覆盖着 wait() 内部作用域的闭包,它早就会消失了。
- 在 引擎 的内脏深处,内建的工具 setTimeout(..) 拥有一些参数的引用,可能称为 fn 或者 func 或者其他诸如此类的东西。引擎 去调用这个函数,它调用我们的内部 timer 函数,而词法作用域依然完好无损。
循环 + 闭包
1 | for (var i=1; i<=5; i++) { |
这段代码的精神是,我们一般将 期待 它的行为是分别打印数字“1”,“2”,……“5”,一次一个,一秒一个
实际上,如果你运行这段代码,你会得到“6”被打印5次,一秒一个。
我们试图 暗示 在迭代期间,循环的每次迭代都“捕捉”一份对 i 的拷贝。但是,虽然所有这5个函数在每次循环迭代中分离地定义,由于作用域的工作方式,它们 都闭包在同一个共享的全局作用域上,而它事实上只有一个 i
如何解决这个问题呢,定义一个新的作用域,在每次迭代时持有值 i 的一个拷贝。在新的匿名函数内部定义了一个新的局部作用域,i设置为了每次遍历时的值,这样便不会继续往上遍历了。
1 | for (var i=1; i<=5; i++) { |