函数作用域
函数作用域
1 | var name = 'nick' |
在上面的这段代码中,name
处在全局作用域下;foo
函数也是在全局中,它所处的作用域也是全局。
改变一下代码:
1 | var name = 'nick' |
现在,name
和foo
所处的作用域环境没变,bar
函数处在两个作用域中,一个是全局作用域,另一个是foo
函数的作用域。
都知道的一个知识点是函数内部的作用域是可以访问外部的作用域,而外部作用域不可以访问内部作用域。当函数执行时,会从自身内部作用域开始查找所需要的属性,若是找到所需要的属性,便会停止寻找;没有找到,便会向外查找,一直查找到最外层作用域,也就是全局作用域;如果全局作用域没有,那就会报错undefined
。
词法作用域
我最初对与作用域的理解便是这样,直到后来看到冴羽大佬的这篇文章:《JavaScript 深入之词法作用域和动态作用域》,发现就像是打开了新世界的大门一般。
文章中说,JavaScript 采用的是词法作用域,也就是静态作用域。就是说,JavaScript 函数的作用域在函数定义的时候就决定了。相反的,动态作用域则是在函数调用的时候才决定的。
1 | var value = 1 |
上面这段代码,按照我之前的理解,我有可能会认为打印出来的值为2
,也有可能为1
。这是因为foo
函数需要查找value
这个属性,而它本身内部是没有这个属性的,但是它同时存在于全局作用域与bar
函数的作用域中,所以说,我就会比较疑惑。
但是搞懂了词法作用域之后,一下子就明白许多了。函数的作用域是在函数定义的时候就确定了,也就是说foo
函数是在全局作用域下定义的,那么它本身就处在全局作用域下。尽管是在bar
函数作用域中调用,但是因为在定义时,就确定了作用域,所以,foo
函数会从自身开始查找value
属性,没找到,就回去外层作用域,也就是全局作用域。所以说,最后的结果为1
。
现在来做个小改动,将bar
函数中的var
关键字去掉:
1 | var value = 1 |
现在的结果是多少呢?答案是2
。
整个函数的执行过程没有发生改变,作用域也没发生改变。按照词法作用域的规则,foo
此时的外部作用域依然处在全局中,同样会在全局作用域中查找value
的值。但是bar
函数在执行过程中,内部没用通过关键字声明value
,会自动挂载在全局中,不受函数内部作用域的控制。然后因为全局里面已经声明了value
了,所以,bar
函数内部的value
的值会覆盖全局中value
的值。改变之后,foo
函数才执行,向外寻找value
时,发现已经改变成为了2
,所以,最后打印结果为2
。
作用域链
还有一个东西叫做作用域链,在冴羽大佬的《JavaScript 深入之作用域链》,详细介绍了有关作用域链的东西。我在此,记录一下笔记。
之前得知,JavaScript 采用的是词法作用域,函数在创建时,就已经确定了作用域;因为在函数中有一个内置属性[[scope]]
,在函数创建时,会将所有父变量(我的理解就是外层作用域,也可以说所处作用域)对象保存起来。
1 | function foo() { |
当函数创建时,foo
和bar
各自的[[scope]]
属性为:
1 | foo.[[scope]] = [ |
需要注意的是,只有
foo
函数执行时,bar
函数才会被创建,然后[[scope]]
属性开始保存所有的父变量。
当函数执行时,进入函数上下文,并且 AO/VO 创建后,会将活动对象(也就是 AO)添加到作用域链的顶端。
这时候执行上下文的作用域链,我们命名为 Scope
。
简单的例子
1 | function foo() { |
执行过程
- foo 函数被创建,将父变量对象保存在内置属性
[[scope]]
中:
1 | foo.[[scope]] = [ |
- 准备执行 foo 函数,这时创建 foo 函数的执行上下文,并将其压入执行上下文栈:
1 | ECStack = [fooContext, globalContext] |
- 进入到 foo 函数的上下文,复制
[[scope]]
到作用域链:
1 | fooContext = { |
- 创建活动对象(arguments,函数内声明的变量等):
1 | fooContext = { |
- 将活动对象放在作用域链的顶端:
1 | fooContext = { |
- 开始执行,修改活动对象的值:
1 | fooContext = { |
- 在作用域链中找到
name
的值,然后函数执行完毕,将foo
函数的函数上下文从执行上下文栈中弹出:
1 | ECStack = [globalContext] |
总结
- 创建函数时,首先会将父变量(外层作用域的 VO/AO)保存在函数的内置属性
[[scope]]
中 - 创建完毕,准备执行时,创建函数的上下文并且将其压入在上下文栈中,同时创建作用域链,复制
[[scope]]
并将其保存在作用域链中 - 创建 AO,创建完毕后,会将 AO 放在作用域链的顶端
- 函数开始执行,改变 AO 内的值,在作用域链中开始查找需要的属性
- 函数执行完毕,函数上下文从上下文栈中弹出