函数作用域

js
1
2
3
4
5
6
7
var name = 'nick'

function foo() {
console.log(name)
}

foo()

在上面的这段代码中,name处在全局作用域下;foo函数也是在全局中,它所处的作用域也是全局。

改变一下代码:

js
1
2
3
4
5
6
7
8
9
10
11
12
var name = 'nick'

function foo() {
var age = 18

function bar() {
console.log(age, name)
}
bar()
}

foo()

现在,namefoo所处的作用域环境没变,bar函数处在两个作用域中,一个是全局作用域,另一个是foo函数的作用域。

都知道的一个知识点是函数内部的作用域是可以访问外部的作用域,而外部作用域不可以访问内部作用域。当函数执行时,会从自身内部作用域开始查找所需要的属性,若是找到所需要的属性,便会停止寻找;没有找到,便会向外查找,一直查找到最外层作用域,也就是全局作用域;如果全局作用域没有,那就会报错undefined

词法作用域

我最初对与作用域的理解便是这样,直到后来看到冴羽大佬的这篇文章:《JavaScript 深入之词法作用域和动态作用域》,发现就像是打开了新世界的大门一般。

文章中说,JavaScript 采用的是词法作用域,也就是静态作用域。就是说,JavaScript 函数的作用域在函数定义的时候就决定了。相反的,动态作用域则是在函数调用的时候才决定的。

js
1
2
3
4
5
6
7
8
9
10
11
12
var value = 1

function foo() {
console.log(value)
}

function bar() {
var value = 2
foo()
}

bar()

上面这段代码,按照我之前的理解,我有可能会认为打印出来的值为2,也有可能为1。这是因为foo函数需要查找value这个属性,而它本身内部是没有这个属性的,但是它同时存在于全局作用域与bar函数的作用域中,所以说,我就会比较疑惑。

但是搞懂了词法作用域之后,一下子就明白许多了。函数的作用域是在函数定义的时候就确定了,也就是说foo函数是在全局作用域下定义的,那么它本身就处在全局作用域下。尽管是在bar函数作用域中调用,但是因为在定义时,就确定了作用域,所以,foo函数会从自身开始查找value属性,没找到,就回去外层作用域,也就是全局作用域。所以说,最后的结果为1

现在来做个小改动,将bar函数中的var关键字去掉:

js
1
2
3
4
5
6
7
8
9
10
11
12
var value = 1

function foo() {
console.log(value)
}

function bar() {
value = 2
foo()
}

bar()

现在的结果是多少呢?答案是2

整个函数的执行过程没有发生改变,作用域也没发生改变。按照词法作用域的规则,foo此时的外部作用域依然处在全局中,同样会在全局作用域中查找value的值。但是bar函数在执行过程中,内部没用通过关键字声明value,会自动挂载在全局中,不受函数内部作用域的控制。然后因为全局里面已经声明了value了,所以,bar函数内部的value的值会覆盖全局中value的值。改变之后,foo函数才执行,向外寻找value时,发现已经改变成为了2,所以,最后打印结果为2

作用域链

还有一个东西叫做作用域链,在冴羽大佬《JavaScript 深入之作用域链》,详细介绍了有关作用域链的东西。我在此,记录一下笔记。

之前得知,JavaScript 采用的是词法作用域,函数在创建时,就已经确定了作用域;因为在函数中有一个内置属性[[scope]],在函数创建时,会将所有父变量(我的理解就是外层作用域,也可以说所处作用域)对象保存起来。

js
1
2
3
4
5
function foo() {
function bar() {
//
}
}

当函数创建时,foobar各自的[[scope]]属性为:

js
1
2
3
4
5
6
7
8
foo.[[scope]] = [
golbalContext.VO
]

bar.[[scope]] = [
fooContext.AO
golbalContext.VO
]

需要注意的是,只有foo函数执行时,bar函数才会被创建,然后[[scope]]属性开始保存所有的父变量。

当函数执行时,进入函数上下文,并且 AO/VO 创建后,会将活动对象(也就是 AO)添加到作用域链的顶端。

这时候执行上下文的作用域链,我们命名为 Scope

简单的例子

js
1
2
3
4
5
6
function foo() {
var name = 'nick'
return name
}

foo()

执行过程

  1. foo 函数被创建,将父变量对象保存在内置属性[[scope]]中:
js
1
2
3
foo.[[scope]] = [
globalContext.VO
]
  1. 准备执行 foo 函数,这时创建 foo 函数的执行上下文,并将其压入执行上下文栈:
js
1
ECStack = [fooContext, globalContext]
  1. 进入到 foo 函数的上下文,复制[[scope]]到作用域链:
js
1
2
3
fooContext = {
Scope: foo.[[scope]]
}
  1. 创建活动对象(arguments,函数内声明的变量等):
js
1
2
3
4
5
6
7
8
9
fooContext = {
AO: {
arguments: {
length: 0
},
name: undefined
},
Scope: foo.[[scope]]
}
  1. 将活动对象放在作用域链的顶端:
js
1
2
3
4
5
6
7
8
9
fooContext = {
AO: {
arguments: {
length: 0,
},
name: undefined,
},
Scope: [AO, [[Scope]]],
}
  1. 开始执行,修改活动对象的值:
js
1
2
3
4
5
6
7
8
9
fooContext = {
AO: {
arguments: {
length: 0,
},
name: 'nick,
},
Scope: [AO, [[scope]]],
}
  1. 在作用域链中找到name的值,然后函数执行完毕,将foo函数的函数上下文从执行上下文栈中弹出:
js
1
ECStack = [globalContext]

总结

  • 创建函数时,首先会将父变量(外层作用域的 VO/AO)保存在函数的内置属性[[scope]]
  • 创建完毕,准备执行时,创建函数的上下文并且将其压入在上下文栈中,同时创建作用域链,复制[[scope]]并将其保存在作用域链中
  • 创建 AO,创建完毕后,会将 AO 放在作用域链的顶端
  • 函数开始执行,改变 AO 内的值,在作用域链中开始查找需要的属性
  • 函数执行完毕,函数上下文从上下文栈中弹出