作用域
与『执行上下文』不同,作用域在 V8 中并无具体代码实现。 本质为一套规则,规定变量与函数的可访问范围。即作用域控制着变量和函数的可见性和生命周期。目的在于隔离变量,保证不同作用域下同名变量不会冲突。
在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。
- 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
- 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。
/* 全局作用域开始 */
var a = 1
function func() {
/* func 函数作用域开始 */
var a = 2
console.log(a)
} /* func 函数作用域结束 */
func() // => 2
console.log(a) // => 1
/* 全局作用域结束 */ 0
块作用域
由 let 或 const 加上一组大括号构成。在大括号之外不能访问括号内的变量。** 前文提过,块作用域在执行上下文通过词法环境以栈结构存储提现。**
if (true) {
let a
}
console.log(a) // ReferenceError: a没有定义
while (true) {
let b
}
console.log(b) // ReferenceError: b没有定义
function foo() {
let c
}
console.log(c) // ReferenceError: c没有定义
{
let d
} //单独的块也是 let 声明变量的作用域。
console.log(d) // ReferenceError: d没有定义
作用域链
当可执行代码内部访问变量时,会先查找作用域,如果找到目标变量即返回,否则会去父级作用域继续查找...一直找到全局作用域。我们把这种作用域的嵌套机制,称为 『作用域链』。
let foo = 'foo'
function bar() {
let baz = 'baz'
// 打印 'baz'
console.log(baz)
// 打印 'foo'
console.log(foo)
number = 42
console.log(number) // 打印 42
}
bar()
当函数『bar()』被调用,Javascript 引擎首先在当前作用域下寻找变量『baz』,然后寻找 foo 变量但发现在当前作用域下找不到,然后继续在外部作用域寻找找到了它(这里是在全局作用域找到的)。 然后将 42 赋值给变量 number。Javascript 引擎会在当前作用域以及外部作用域下一步步寻找 number 变量(没找到)。 如果是在非严格模式下,引擎会创建一个 number 的全局变量并把 42 赋值给它。但如果是严格模式下就会报错了。
词法作用域
作用域本质是一套规则,而这个规则的底层遵循的就是词法作用域模型。 从语言的层面来说,作用域模型分两种:
- 词法作用域:也称静态作用域,是最为普遍的一种作用域模型
- 动态作用域:相对“冷门”,bash 脚本、Perl 等语言采纳的是动态作用域
假设有如下代码:
var value = 1
function foo() {
console.log(value)
}
function bar() {
var value = 2
foo()
}
bar()
// 结果是 ???
上面这段代码中,一共有三个作用域:
- 全局作用域
- 『foo』 的函数作用域
- 『bar』 的函数作用域
foo 里访问了本地作用域中没有的变量 value 。根据前面说的,引擎为了拿到这个变量就要去 foo 的上层作用域查询,那么 foo 的上层作用域是什么呢?是它 调用时 所在的 bar 作用域?还是它 定义时 所在的全局作用域? 这个关键的问题就是 javascript 中的作用域类型——词法作用域。
词法作用域,就意味着
- **函数被定义的时候,它的作用域就已经确定了,和拿到哪里执行没有关系,因此词法作用域也被称为 “静态作用域”。 **
- 内部函数总是可以访问其外部函数中声明的变量。(闭包紧密相关)
如果是动态作用域类型,那么上面的代码运行结果应该是 bar 作用域中的 2 。
我并未在作用域做过多展开,而是将更多底层的解释放在了上下文。事实上在我的认知里,作用域只是执行上下文的前置规则。 作用域决定上下文中的变量 key 值,而上下文决定 value 值。