什么是作用域?
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域
静态作用域 & 动态作用域?
- 静态作用域: 函数的作用域在定义时就确定了
- 动态作用域: 函数的作用域在函数调用时才确定
看一个例子:
var value = 1;
function foo() {
console.log(value); // ?
}
function bar() {
var value = 2;
foo();
}
bar()
复制代码
假设从静态作用域的角度看这题:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
假设从动态作用域的角度看这题:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
上面说过, javascript采用的是静态作用域, 所以输出的是1
执行上下文
javascript引擎在执行代码时并不是一行一行去执行的,而是一块一块去执行的,引擎在一块一块去执行之前, 会先进行一个准备工作,就是词法分析,比如变量提升,函数提升,重复提升过滤等。《你不知道的jacvascript》专门有讲javascript词法分析。
举个例子:
我们可以把一个js文件看作是一块代码, javascript引擎在执行这块代码之前会进行一个词法分析, 然后再去执行
console.log(value) // undefined
var value = 'hello world'
复制代码
这就是为什么上面代码输出是 undefined 而不是 value is not defined
这里有个疑问就是javascript是怎么区分一块一块代码的呢?
在es6之前, 就三种: 全局代码、 函数、 eval代码
这里又要引入一个 执行上下文栈(Execution context stack,ECS))的概念, 假设
ECS = []
用一个数组来表示执行上下文栈
也就是说, javascript引擎在执行全局代码的时候首先会向
ECS栈中压入全局上下文(globalContext), 之后遇到函数时,又向ECS中压入函数执行上下文,当函数执行完毕,再从ECS中弹出对应执行上下文
javascript引擎就是通过执行上下文栈来管理各种执行上下文的
执行上下文中有三个重要的属性:
-
变量对象(Variable object,VO)
-
this
-
作用域链(作用域链)
variable object + all parents scopes, 即包含变量对象和所有父级维护的私有作用域([[scope]])
每个执行上下文都会分配一个变量对象,变量对象的属性由变量声明和函数声明构成, 在函数执行上下文中, 变量对象还会包括形参(argument)
在函数执行上下文中,还会多一个活动对象(activation object,ao)的概念。
当函数被激活,那么一个活动对象(activation object)就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象(variable object)用于变量初始化
即: 在函数上下文中:
variableObject = activationObject + variableObject
举个例子:
function test(num){
var a = "2";
return a+num;
}
复制代码
在上面test函数上下文中,变量对象不仅包含了变量 a, 还包括了形参 num, 简单来说, 可以将变量对象和活动对象看作是一个东西,
只不过函数的变量对象还要加上形参。
(这里说的不对, 不过可以这样去简单记忆 – -)
关于作用域链:
每个函数在创建的时候, 函数内部都会维护一个[[scope]]的属性
举个例子:
function foo() {
function bar() {
...
}
}
复制代码
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
复制代码
这个可能不是很直观:
再看一个例子:
最后看一个经典题(简化了一下):
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope()
复制代码
上面输出是什么? 具体执行过程是什么?
-
进入全局环境上下文,全局环境被压入环境栈,contextStack = [globalContext]
-
全局上下文环境初始化,
globalContext={
variable object:[scope, checkscope],
scope chain: variable object // 全局作用域链
}
复制代码
同时checkscope函数被创建,此时 checkscope.[[Scope]] = globalContext.scopeChain
-
执行checkscope函数,进入checkscope函数上下文,checkscope被压入环境栈,contextStack=[checkscopeContext, globalContext]。随后checkscope上下文被初始化,它会复制checkscope函数的[[Scope]]变量构建作用域,即 checkscopeContext={ scopeChain : [checkscope.[[Scope]]] }
-
checkscope的活动对象被创建 此时 checkscope.activationObject = [arguments], 随后活动对象被当做变量对象用于初始化,checkscope.variableObject = checkscope.activationObject = [arguments, scope, f],随后变量对象被压入checkscope作用域链前端,(checckscope.scopeChain = [checkscope.variableObject, checkscope.[[Scope]] ]) == [[arguments, scope, f], globalContext.scopeChain]
-
函数f被初始化,f.[[Scope]] = checkscope.scopeChain。
-
checkscope执行流继续往下走到 return f(),进入函数f执行上下文。函数f执行上下文被压入环境栈,contextStack = [fContext, checkscopeContext, globalContext]。函数f重复 第4步 动作。最后 f.scopeChain = [f.variableObject,checkscope.scopeChain]
-
函数f执行完毕,f的上下文从环境栈中弹出,此时 contextStack = [checkscopeContext, globalContext]。同时返回 scope, 解释器根据f.scopeChain查找变量scope,在checkscope.scopeChain中找到scope(local scope)。
-
checkscope函数执行完毕,其上下文从环境栈中弹出,contextStack = [globalContext]
以上就是我对javascript 执行上下文, 作用域的一些理解, 又很多地方讲的不严谨, 如果需要深挖一下, 建议阅读以下参考文章
参考文章: