一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情。
作用域(scope)
作用域(scope),它规定了如何去查找变量的规则。通俗点就是说当前执行代码对变量的访问权限
比如我当前定义了这三个值 a
b
c
1 | var a = 1 |
声明
变量只有在声明之后才能在作用域中查找到它
1 | function f(a){ |
1 | function f(a){ |
上面代码实际等同于
1 |
|
如何声明
var
:在编译器解析时,会将var a = 1
视为var a
a = 1
两段执行,存在变量提升!let
:重复声明报错,属块级作用域,存在变量提升,但会暂时性死区const
:重复声明报错,属块级作用域,存在变量提升,但会暂时性死区。内容只读,修改报错,但引用类型保存的是指针function
:函数声明,直接提升到最上面,提升优先级最高,且已完成赋值!
静态作用域(词法作用域)
静态作用域(词法作用域),采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见;在这段区域以外该变量不可见。如图 scope1
与 scope2
是互相隔离的,作用域链沿定义的位置往外延伸
词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。
全局作用域
全局作用域:声明在任何函数之外的顶层作用域的变量就是全局变量,变量拥有全局作用域
1 | var a = 0 // 最外层声明,全局变量 |
函数作用域(局部作用域)
全局作用域:声明在函数内的顶变量,拥有函数作用域,外部环境无法访问到函数内部的变量(模块化的原理)
1 | function a(){ |
块作用域(局部作用域)
块作用域:ES6
开始,使用 let
和 const
声明的变量拥有块级作用域,作用域范围在 {}
之间
1 | { |
为什么需要块级作用域
- 解决声明提前
1
2
3
4console.log(a) // undefined
var a = 1
console.log(b) // 报错
let b = 2 - 解决
{}
中var
声明被视为全局变量1
2
3
4
5
6
7
8
9{
var c = 1
}
console.log(c) // 1
{
let d = 1
}
console.log(c) // 报错
看实际的例子
1 | var val = 'global' |
1 | function fn(){ |
修改词法作用域的方式 eval
与 with
(最好不要用!)
eval
1
2
3
4
5function fn(fnStr){
eval(fnStr)
}
fn('a = "a"')
console.log(a) // awith
1
2
3
4
5
6
7
8
9var a = {
b: 'b'
}
with(a){
b = 'change'
c = 'c' // 非严格模式查找键值不存在,会创建一个全局变量!
}
console.log(a.b) // 'change'
console.log(c) // c
动态作用域
动态作用域,采用变量叫动态变量。程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。
作用域链沿着调用栈往外延伸,通过逐层检查函数的调用链,并打印第一次遇到的值。
如果是动态作用域
1 | var local = 'in global' |
实际上执行是这样的
1 | var local = 'in global' |
无论你在哪个位置调用 B
都只会向上查找到 in global
编译
1 | var a = 1 |
- 词法分析:将字符打断成为有意义的片段
(token)
- 比如上面的声明会被打断成如下
token
辅助工具 var a = 1
=>var
a
=
1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "1"
}
]
- 比如上面的声明会被打断成如下
- 解析:将每个
token
数组转换成一个嵌套元素的树,也就是抽象语法树AST(Abstract Syntax Tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
]
} - 代码生成:将抽象语法树转换成可执行代码
执行上下文(Execution Context)
当 JavaScript
被解析执行时,需要 执行代码的环境
这个环境被称为 执行上下文
分类
- 全局上下文:全局代码所处的环境,不在函数内的代码均执行与全局上下文中
- 函数上下文:函数调用时创建的环境
- eval上下文:运行
eval
函数中代码时创建的环境
生命周期
- 创建阶段:此时还未执行代码,只做了准备工作
- 创建变量对象:
arguments
,提升函数声明和变量声明 - 创建作用域链:用于解析变量,从内层开始查找,逐步往外层词法作用域中查找
- 确定
this
- 创建变量对象:
- 执行阶段:开始执行代码,完成变量赋值,函数引用等等
- 回收阶段:函数调用完毕后,函数,对应的执行上下文出栈,等待垃圾回收器回收
1 | var e = 0 |
- 全局上下文创建阶段
- 全局上下文执行阶段
- 遇到函数调用
a(1)
a
函数上下文创建阶段,入栈
a
函数上下文执行阶段
c()
函数调用,c
函数上下文创建阶段,入栈
c
执行完毕,出栈
a
执行完毕,出栈
特点
- 全局执行上下文在代码开始时创建,有且只有一个,且永远再栈底
- 函数被调用时就会创建函数执行上下文,后入栈。(根据调用创建)
变量对象(Variable Object,VO)
变量对象时上下文相关的数据作用域,存储了上下文定义的变量和函数声明
1 | var e = 0 |
作用域链(Scope Chain)
多个变量对象构成的链表则为作用域链(Scope Chain),从离它最近的变量对象(VO)开始查找变量,逐级往上