上下文与作用域之间有什么样的关系?
这一概念看似简单,但很多人都讲不清楚之间的关系。上下文和作用域都是编译原理的知识,具体编程语言有具体的实现规则,本文关注的是 JavaScript 语言的实现。
一、 上下文与作用域
上下文
(context)是一段程序运行所需要的最小数据集合。我们可以从上下文交换
(context switch)来理解上下文,在多进程或多线程环境中,任务切换时首先要中断当前的任务,将计算资源交给下一个任务。因为稍后还要恢复之前的任务,所以中断的时候要保存现场,即当前任务的上下文,也可以叫做环境。
作用域
(scope)是标识符(变量)在程序中的可见性范围。作用域规则是按照具体规则维护标识符的可见性,以确定当前执行的代码对这些标识符的访问权限。作用域是在具体的作用域规则之下确定的。
上下文、环境有时候也称作用域,即这两个概念有时候是混用的;不过,上下文指代的是整体环境,作用域关注的是标识符(变量)的可访问性(可见性)。上下文确定了,根据具体编程语言的作用域规则,作用域也就确定了。这就是上下文与作用域的关系。
1 | function callWithContext(fn, context) { |
1 | var a = 1; |
二、 JavaScript的执行
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
当JavaScript代码执行进入一个环境时,就会为该环境创建一个执行上下文
,它会在你运行代码前做一些准备工作,如确定作用域,创建局部变量对象等。
JS代码的执行环境
- 全局环境
- 函数环境
- eval函数环境(不推荐使用)
执行上下文的类型
- 全局执行上下文
- 函数执行上下文
- eval函数执行上下文
三、 执行上下文
JavaScript运行时首先会进入全局环境,对应会生成全局上下文。程序代码中基本都会存在函数,那么调用函数,就会进入函数执行环境,对应就会生成该函数的执行上下文。
函数编程中,代码中会声明多个函数,对应的执行上下文也会存在多个。在JavaScript中,通过栈的存取方式来管理执行上下文,我们可称其为执行栈,或函数调用栈(Call Stack)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。
程序执行进入一个执行环境时,它的执行上下文就会被创建,并被推入执行栈中(入栈);程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。栈结构
因为JS执行中最先进入全局环境,所以处于”栈底的永远是全局环境的执行上下文”。而处于”栈顶的是当前正在执行函数的执行上下文”,当函数调用完成后,它就会从栈顶被推出。
“全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底”
1 | let color = 'blue'; |
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈
- 函数的执行上下文的个数没有限制
- 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此
四、 词法作用域
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
在 JavaScript 中,这个具体的作用域规则就是词法作用域
(lexical scope),也就是 JavaScript 中的作用域链的规则。词法作用域是的变量在编译时(词法阶段)就是确定的,所以词法作用域又叫静态作用域
(static scope),与之相对的是动态作用域
(dynamic scope)。
1 | let a = 2; |
前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。虽然在 bar 内调用,但由于 foo 是闭包函数,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域。所谓闭包函数
,即这个函数封闭了它自己的定义时的环境
,形成了一个闭包
。(即闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量)所以 foo 并不会从 bar 中寻找变量,这就是静态作用域的特点。
而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
词法作用域是在写代码或者定义时确定的,而动态作用域是在运行时确定的。词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
1 | function foo() { |
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
1 | var x = 1; |
上面代码中,函数foo
的参数形成一个单独作用域。这个作用域里面,首先声明了变量x
,然后声明了变量y
,y
的默认值是一个匿名函数。这个匿名函数内部的变量x
,指向同一个作用域的第一个参数x
。函数foo
内部又声明了一个内部变量x
,该变量与第一个参数x
由于不是同一个作用域,所以不是同一个变量,因此执行y
后,内部变量x
和外部全局变量x
的值都没变。
五、 闭包的应用
模块化、柯里化、模拟块级作用域、命名空间、缓存数据
1 | const tar = (function () { |
1 | let add = function(x){ |
1 | for (var i = 1; i < 5; i++) { |
1 | var MyNamespace = {}; |
1 | import {readFileSync} from 'fs'; |
参考文章: