Appearance
作用域&上下文
在文章开始前必须要知道作用域和上下文的区别
作用域:
作用域(Scope)指的是在程序中定义变量的区域,它决定了变量的可见性和生命周期。作用域规定了在代码中访问变量的规则。在 JavaScript 中,主要有全局作用域和函数作用域。全局作用域中的变量可以在整个程序中访问,而函数作用域中的变量则只能在函数内部访问。作用域链用于在嵌套的作用域中查找变量。
上下文:
上下文(Context)指的是代码执行时所处的环境,它包含了当前代码的执行状态和相关信息。在 JavaScript 中,主要有全局上下文和函数上下文。全局上下文是在程序启动时创建的,而函数上下文是在函数调用时创建的。每个上下文都有自己的变量对象(Variable Object)用于存储变量、函数和其他相关信息。
作用域和上下文之间的关系是这样的:
作用域决定了变量的可见性和访问规则,而上下文提供了执行代码所需的环境和状态。作用域规定了在代码中如何查找变量,而上下文则提供了变量的具体值和其他执行时的信息。
简单来说,作用域关注的是变量的可见性和访问规则,而上下文关注的是代码执行时的环境和状态。
上下文:
在每个函数开始之初,执行上下文中会创建变量环境和词法环境(还有一个私有环境,因为使用较少就不展开),并将当前函数执行上下文推入执行上下文栈中,当函数执行结束后会让执行上下文栈出栈
- 变量环境:
- 环境记录:包含
var
声明和function
声明的变量 - 外部环境引用
- 环境记录:包含
- 词法环境:
- 环境记录:包含
let
和const
声明的变量 - 外部环境引用
- 环境记录:包含
外部环境的引用借用概念就等同于链表的.next
指针,那么.value
就为环境记录了
环境记录:
环境记录(Environment Record)是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构,定义标识符与特定变量和函数的关联。环境记录通常与 ECMAScript 代码的某些特定语法结构相关联。
例如函数声明(FunctionDeclaration)、块级作用域(BlockStatement) 或 try...catch(TryStatement) 的 Catch 子句。每次评估这些代码时,都会创建一个新的环境记录(Environment Record),以记录该代码创建的标识符绑定。
每个环境记录都有一个[[OuterEnv]]字段,该字段要么为空,要么是对外部环境记录的引用。这个字段用于模拟环境记录值的逻辑嵌套。内部环境记录的外部引用是对逻辑上围绕内部环境记录的环境记录的引用。当然,外部环境记录也可以有自己的外部环境记录。一个环境记录可以作为多个内部环境记录的外部环境。
例如,如果一个 FunctionDeclaration 包含两个嵌套的 FunctionDeclaration,那么每个嵌套函数的环境记录都将以周围函数的当前评估环境记录作为其外部环境记录。
这段官方文档的解释与
现代JavaScript教程
存在出入,具体在于——外部环境引用是存在于环境中
还是环境记录
中,为了统一认知理解,这里我们统一以现代JavaScript教程的在环境中
为例讲解
环境记录纯粹是一种规范机制,无需与 ECMAScript 实现中的任何具体工件相对应。ECMAScript 程序不可能直接访问或管理环境记录。
核心要点:
var
和function
声明的变量会存入最近的变量环境,而const
和let
声明的变量会存入最近的词法环境
一个上下文中的变量会有三个状态:
- 声明(Declaration):当在代码中使用
var
、let
、const
或函数声明时,会进行变量的声明。在这个阶段,变量的标识符被引入到当前作用域中,但尚未分配内存空间,也没有初始化值。 - 创建(Creation):在进入变量的作用域时,会创建该变量的内存空间。这个阶段被称为变量的创建阶段。在创建阶段,变量会被分配内存空间,并绑定到相应的作用域中。
- 赋值(Assignment):在变量创建后,可以对变量进行赋值操作。赋值阶段是给变量分配一个具体的值或引用的过程。变量的赋值可以在声明的同时进行,也可以在稍后的代码中进行。
对于 var
和function
声明的变量,会在作用域的顶部进行提升(hoisting),即在变量的作用域开始之前就进行了创建
对于 let
和 const
声明的变量,则是在块级作用域中实际声明的位置进行创建。
这里还有一点,function
提升的优先级是高于var
的
javascript
console.log(typeof a);// function
var a = 2
function a() { return 1 }
1
2
3
2
3
作用域:
作用域总共分为三种:
全局作用域
函数作用域
块级作用域
全局作用域和函数作用域都是老生常谈的问题了,除此之外的由{}
包裹的就是块级作用域。
当程序执行到函数时会创建函数执行上下文,然后会创建变量环境和词法环境
当程序在函数中执行到块级作用域时,不会创建新的上下文,只会创建一个新的词法环境
该词法环境的环境记录会存储块级作用域中由
const
和var
声明的变量,outer
指向当前外层(也许是函数执行上下文)的词法环境,当块级作用域部分执行结束后,词法环境会被销毁
举例如下:
ts
const name = "luowei";
function test1(){
console.log(name); // luowei
}
function test2(){
const name = "passionfruit"
test1();
}
function main() {
console.log(a); // undefined
console.log(b); // undefined
{
const test1 = 2;
var a = 1
function b() {
test2();
}
console.log(test1); // 2
}
b();
}
main();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
以上面这个函数为例我们逐步分析函数执行过程:
编译并创建全局执行上下文,初始化词法环境和变量环境
进行全局执行上下文的变量声明
js// 全局变量环境 const globalVariableEnvironment = { test1: undefined, test2: undefined, main: undefined } // 全局词法环境 const globalLexicalEnvironment = { name: <uninitialization> }
1
2
3
4
5
6
7
8
9
10
11进行全局执行上下文的变量创建
js// 全局变量环境 const globalVariableEnvironment = { test1: undefined, test2: undefined, main: undefined } // 全局词法环境 const globalLexicalEnvironment = { name: undefined }
1
2
3
4
5
6
7
8
9
10
11进行全局执行上下文的变量赋值
js// 全局变量环境 const globalVariableEnvironment = { test1: function(){...}, test2: function(){...}, main: function(){...} } // 全局词法环境 const globalLexicalEnvironment = { name: "luowei" }
1
2
3
4
5
6
7
8
9
10
11执行main函数
进行
main
函数执行上下文的变量声明js// main变量环境 const mainFnVariableEnvironment = { a: undefined, b: undefined } // main词法环境 const mainFnLexicalEnvironment = { [[outer]]: ... }
1
2
3
4
5
6
7
8
9
10对
a
和b
进行console.log
遇到
{}
块级作用域进行块级作用域词法环境的初始化
js// 块级作用域词法环境 const blockLexicalEnvironment = { test: <uninitialization> [[outer]]: ... }
1
2
3
4
5进行
main
函数执行上下文的变量赋值js// main变量环境 const mainFnVariableEnvironment = { // a b均被变量提升至最近的变量环境 a: 1, b: function(){...} } // main词法环境 const mainFnLexicalEnvironment = { [[outer]]: ... } // 块级作用域词法环境 const blockLexicalEnvironment = { test: 2, [[outer]]: mainFnLexicalEnvironment }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17执行
b
函数blockLexicalEnvironment
被销毁后略...
变量查找时,从自身的词法环境环境记录开始沿着outer
查找,这就是作用域链
这里其实有一个问题:
既然是延词法环境链向上查找,那存在于变量环境中(也就是var声明的值)怎么被查找?
按照我的理解,在在某个环境中进行查找时,如果同时存在词法环境和变量环境,会进行一个合并查找
重点讲一下为什么test2
函数调用test1
时,name
值为"luowei"
,或者说为什么test1
的执行上下文中词法环境的outer
指向全局执行上下文的词法环境
这里我们要说一个新名词叫词法作用域
词法作用域就是指作用域是由代码中的函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符
所以,词法作用域是编译阶段就决定好的,和函数怎么调用的没有一点关系
那闭包又是什么?
我相信前面看完这个问题也很清晰了,闭包产生后函数的执行上下文就没有出栈,如果闭包返回了了一个函数,这个函数执行上下文中的词法环境和变量环境的outer
指向声明时的函数上下文中词法环境和变量环境
在JavaScript中,根据词法作用域的规则,内部函数总是可以访问外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包
所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为
[[Environment]]
的隐藏属性,该属性保存了对创建该函数的词法环境的引用。 ——《现代JavaScript教程》
参考文章:
浏览器工作原理与实践[李兵]——极客时间