作用域
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。作用域包括全局作用域、函数作用域、块级作用域。当作用域进行嵌套的时候,就会形成作用域链。
词法作用域与动态作用域
作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域。另外一种叫做动态作用域,仍然有一些语言在使用(比如bash脚本)。需要明确的是,事实上JavaScript并不具有动态作用域。它只有词法作用域,简单明了。但是this机制某种程度上很像动态作用域。
主要区别
词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的(this也是)。词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
举例
对于下面这段代码:
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
如何JavaScript具有动态作用域,上面的代码理论上会输出3。为什么会这样?因为当foo()无法找到a的变量引用时,会顺着调用栈在调用foo()的地方查找a,而不是在嵌套的词法作用域链中向上查找。由于foo()是在bar()中调用的,引擎会检查bar()的作用域,并在其中找到值为3的变量a。
但是JavaScript只有词法作用域,实际上这段代码会输出2。我们简单分析一下,bar()调用后,foo()调用,此时执行foo函数,foo函数中需要打印输出a,先在自身的函数作用域中找有没有变量a,这里是没有。然后去声明foo函数的作用域中去找(此处为全局作用域),找到了有变量a,打印输出a的值。假设在全局作用域中也没找到,那么会报错Uncaught ReferenceError: a is not defined
。
我们只要记住词法作用域是在写代码或者说定义时确定的。无论上面的foo函数在哪里被调用,都只会打印输出2。
作用域链
当查找变量的时候,会先从当前上下文(作用域)的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。
函数创建
函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
举个例子:
function foo() {
function bar() {
...
}
}
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO//全局
];
bar.[[scope]] = [
fooContext.AO,//foo
globalContext.VO//全局
];
函数激活
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前面。
这时候执行上下文的作用域链,我们命名为 Scope:
Scope = [AO].concat([[Scope]]);
至此,作用域链创建完毕。
捋一捋
以下面的例子为例
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];