作用域链
举个栗子
作用域和变量有关,先看简单的代码:
var a = 'a'
function foo () {
var b = 'b'
console.log(a)
}
foo() // 'a'
console.log(b) // Uncaught ReferenceError: b is not defined
从上面代码的执行结果可以看出,foo
函数取到了它外部的变量 a
, 而最外层的 console.log(b)
操作并没能取得 foo
函数里面的变量 b
.
上面代码中,最外层的变量对象保存了变量 a
以及函数 foo
,代码执行所需的变量和函数都从变量对象里面取得。 变量对象和当前的执行环境相关联,当前执行环境的代码执行结束后,该环境被丢弃,其中保存的变量和函数也随之被丢弃。
每个函数都拥有自己的执行环境,当函数执行时,执行环境被拾起,函数执行完毕,执行环境被丢弃。
上面代码最外层的 console.log(b)
试图取一个变量 b
, 但是当前的变量对象,也就是最外层的变量对象,并没有保存变量 b
, 所以代码报错。
而 foo
为什么能够取到外部的变量 a
呢?
分析一下 foo
取得 a
的过程。
当 foo
执行的时候, JavaScript
创建了一个活动对象。函数的活动对象是在函数执行时产生的,包含了函数的形参和函数内声明的变量等。 foo
声明的 b
在就 foo
的活动对象里面。在函数的执行环境中,活动对象被当做变量对象。
可是,foo
需要的是 a
啊,foo
自身的变量对象没有这个变量。
不要着急,在创建 foo
的时候,也创建了作用域链,里面包含了外一层的变量对象,保存在函数的 [[Scope]]
属性当中。
在调用 foo
的时候,创建 foo
的执行环境和活动对象,然后复制 [[Scope]]
属性构建作用域链,再把当前活动对象插队放到作用域链的最前面。这个时候,foo
自身的活动对象和外部的变量对象就在这个作用域里面排队。
当 foo
在作用域链的最前面,也就是自身的活动对象里面,找不到变量 b
的时候,就会去作用域链的下一个对象里面查找。
总结一下
作用域链依次保存了当前执行环境的变量对象,以及外层执行环境的变量对象。就像是变量对象在排队,越是内层的变量对象,越在前面。若无法在当前的变量对象找到一个变量,就会顺着作用域链,依次查找外部的变量对象。
闭包
闭包,看着这两个字,顾名思义也思不出什么来,不如先把它从脑中先抛开。
举个栗子
从文章的第一个例子,可以知道,在函数的外部是访问不了函数内部的变量的。
简单粗暴地去访问,当然访问不了,但也可以通过一系列操作,来访问函数内部变量。
看看简单的代码:
function bar () {
var a = 0
return function () {
console.log(a++)
}
}
var fn = bar()
fn() // 0
fn() // 1
fn() // 2
上面的 bar
定义变量 a
为 0
, 并函数返回一个函数,返回的函数打印变量 a
, 然后对变量 a
进行递增操作。
执行 bar
,并将返回的函数赋值给 fn
. 执行 fn
, 从执行结果可以看出,这个全局变量的 fn
居然访问到了 bar
内的变量 a
.
现在去采访一下 fn
, 看看他是怎么做到的。
fn
的值是 bar
函数的返回值,也就是 bar
里面返回的匿名函数。
当匿名函数被创建的时候,和所有函数一样,也是将外部的变量对象保存到自身的 [[Scope]]
属性当中,匿名函数保存的,自然是 bar
的活动对象,里面包含了变量 b
.
现在慢动作回放一下 var fn = bar()
这个操作:
- 初始化
bar
的活动对象 ,将a
保存到活动对象 - 执行
bar
内代码,给a
赋值为0
- 执行
bar
内代码,创建匿名函数,将bar
的活动对象保存到匿名函数[[Scope]]
属性 bar
返回匿名函数,bar
执行完毕,其执行环境被丢弃fn
接受bar
返回的匿名函数
注意到步骤 4, 虽然 bar
的执行环境被丢弃,但是 bar
的活动对象依然在内存中,因为匿名的 [[Scope]]
属性还在引用这个活动对象,而匿名函数赋值给了 fn
, fn
仍然存在于内存中。
执行 fn
, fn
自身的活动对象并没有变量 a
, 所以顺着作用域链查找 a
, 可在 bar
遗留的活动对象中找到。
总结一下
重新捡起闭包这两个字,《JavaScript 高级程序设计》这样定义闭包:
闭包是有权访问另一个函数作用域中变量的函数。
结合上面的例子,可以知道 fn
就是一个闭包,因为它有权访问另一个作用域 bar
内的变量。
块级作用域
举个栗子
看简单的代码:
for (var i = 0; i < 3; i ++) {
if (i === 2) {
var b = 200
}
}
console.log(b) // 200
上面的代码输出了 200
, 可不要被 for
和 if
的花括号施了障眼法,误以为输出结果是 undefined
.
上面的 for
和 if
用花括号包起来的代码块,并没与自己的执行环境,所以上面的代码只有一个全局的执行环境,变量都保存在全局的变量对象里面。这就解释了外层为什么能访问花裤号内部的变量。
再举个栗子
看简单的代码:
for (let i = 0; i < 3; i ++) {
if (i === 2) {
const b = 200
}
}
console.log(i) // Uncaught ReferenceError: i is not defined
console.log(b) // Uncaught ReferenceError: b is not defined
ES6
中的 let
和 const
能够形成块级作用域,用 let
和 const
声明的变量,只能在花括号内部访问。
总结一下
简单地用花括号包起来的代码块,是没有自己的执行环境,形成不了作用域的。而 let
和 const
命令声明的变量,只能在花括号内部访问。
总结
作用域和执行环境有关,函数拥有自己的执行环境,创建活动对象,作为自己的变量对象,就形成了作用域。
而简单的用花括号包起来的代码,没有自己的执行环境,当然也形成不了作用域,但是 let
和 const
命令声明的变量,只能在花括号内部访问。
作用域链将内部执行环境的变量对象与外部的变量对象依次保存,使得内部可以访问外部的变量。
当函数 a
的活动对象被函数外的另一个函数 b
引用,函数 b
就是闭包,可以访问函数 a
内的变量。