作用域
一、概念
与『执行上下文』不同,作用域在 V8 中并无直接的代码实现,但 V8 仍然需要管理作用域(Scope)并存储变量引用。
例如,V8 通过 ScriptScope(全局作用域)、FunctionScope(函数作用域) 和 BlockScope(块作用域) 来管理作用域信息。
作用域的本质是一套规则,它规定了变量与函数的可访问范围,控制标识符的解析顺序,并隔离不同作用域内的变量,以防止命名冲突。
在 ES6 之前,ES 的作用域只有两种:全局作用域 和 函数作用域。
- 全局作用域 中的变量在整个程序运行期间都可访问。在浏览器环境中,它的生命周期通常与页面一致,而在 Node.js 中,它的生命周期与进程相同。
- 函数作用域 是指在函数内部定义的变量或函数只能在该函数内部访问。
javascript
/* 全局作用域开始 */
var a = 1
function func() {
/* func 函数作用域开始 */
var a = 2
console.log(a)
} /* func 函数作用域结束 */
func() // => 2
console.log(a) // => 1
/* 全局作用域结束 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
二、块作用域
块作用域是由 let
或 const
加上一组 {}
大括号构成的。在大括号之外,无法访问括号内的变量。
javascript
if (true) {
let a
}
console.log(a) // ReferenceError: a is not defined
while (true) {
let b
}
console.log(b) // ReferenceError: b is not defined
function foo() {
let c
}
console.log(c) // ReferenceError: c is not defined
{
let d
} // 单独的大括号也是 let 声明变量的作用域
console.log(d) // ReferenceError: d is not defined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
作用域链
作用域链 是一组按照嵌套关系排列的作用域。
当代码访问一个变量时,JavaScript 引擎会先在当前作用域查找,如果找不到,就向外层作用域查找,直到全局作用域。如果全局作用域也找不到该变量,在非严格模式下会创建一个全局变量,而在严格模式下会抛出 ReferenceError
。
javascript
let foo = 'foo'
function bar() {
let baz = 'baz'
console.log(baz) // 'baz'
console.log(foo) // 'foo'
number = 42
console.log(number) // 42(非严格模式下会创建全局变量 number)
}
bar()
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
当 bar()
被调用时:
console.log(baz)
会在bar()
的作用域中找到baz
并打印'baz'
。console.log(foo)
先在bar()
作用域中查找foo
,未找到后,继续在外层作用域(全局作用域)查找,并找到'foo'
。number = 42
由于没有let/const/var
关键字,JavaScript 引擎会沿着作用域链查找number
,如果找不到:- 在 非严格模式 下,会创建一个全局变量
number
并赋值42
。 - 在 严格模式 下,会抛出
ReferenceError
错误。
- 在 非严格模式 下,会创建一个全局变量
词法作用域
JavaScript 采用 词法作用域(Lexical Scope),这意味着:
- 函数的作用域在定义时就确定了,而不是在调用时确定,因此 JavaScript 不支持动态作用域。
- 函数内部始终可以访问外部作用域的变量,即使该函数在不同的作用域中执行,这也是 闭包(Closure) 的基础。
从编程语言的角度,作用域分为两种:
- 词法作用域(Lexical Scope):也称静态作用域,JavaScript、C、Java 等语言都采用词法作用域。
- 动态作用域(Dynamic Scope):相对冷门,Bash 脚本、Perl 等语言采用动态作用域。
词法作用域示例
javascript
var value = 1
function foo() {
console.log(value)
}
function bar() {
var value = 2
foo()
}
bar()
// 结果是 ???
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
在这段代码中,一共有三个作用域:
- 全局作用域
- foo 的函数作用域
- bar 的函数作用域
foo()
内部访问了 value
变量。由于 foo()
自己的作用域中没有 value
,JavaScript 引擎会向上查找:
- 先查找
foo
作用域 → 没有找到value
- 再查找
foo
的上层作用域(即 定义时的作用域,全局作用域)→ 找到value = 1
console.log(value)
输出1
如果 JavaScript 采用动态作用域,那么 foo()
在 bar()
内部执行时,它的 value
变量应该来自 bar()
作用域,而不是全局作用域,因此应该打印 2
。
但由于 JavaScript 采用词法作用域,函数作用域在定义时就确定了,所以 foo()
仍然访问的是全局作用域中的 value
,最终结果是:
javascript
1 // JavaScript 采用词法作用域,foo() 访问的是全局作用域中的 value
1