JavaScript Scope Context
作用域
javascript采用的是词法作用域(lexical scoping),也就是静态作用域
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 ???
结果当然是1,因为foo中的value是在函数定义的时候就决定好了的。
而动态作用域的编程语言,作用域是在函数执行的时候决定的,结果就会是2.
函数声明与函数表达式
看这两段代码输出结果:
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
我们看到,上面这段代码是函数表达式,不存在着变量提升,因此,两个foo之间不会发生覆盖
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
我们看到,上面这段代码是函数声明,其实是存在着变量提升的,因此,下面的foo会把上面的foo覆盖了
执行上下文
javascript的执行上下文有三种:
-
全局执行上下文
先来了解以下全局执行上下文,作为javascript的全局函数和全局属性的占位符。通过使用全局对象,可以访问其他预先定义好的对象、属性与方法。
在node中,全局对象是global
在浏览器中,全局对象是window
console.log(this); // window var a = 1; console.log(this.a); // 1 console.log(Math.random()); // 0.21366211215299735 console.log(this.Math.random()); // 0.8163126334999935
全局上下文中的变量对象,就是全局对象
-
函数执行上下文
在函数上下文中,我们用活动对象来表示全局对象。
具体来看,当变量对象(VO)进入到执行上下文中的时候,变量对象会被激活,然后就变成激活对象(AO)
活动对象是进入了执行上下文的时候才被创建的,它通过函数的arguments属性初始化,arguments的属性值是arguments对象。
-
eval函数执行上下文
我们写的函数很多,js如何管理这么多的执行上下文呢?答案是通过执行上下文栈,我们在这里简称为ECStack
ECStack = [];
试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
ECStack.push(globalContext);
// ECStack = [globalContext];
倘若遇到下面这一段代码:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:
ECStack.push(fun1<funcontext>);
ECStack.push(fun2<funcontext>);
ECStack.push(fun3<funcontext>);
ECStack.pop(fun3<funcontext>);
ECStack.pop(fun2<funcontext>);
ECStack.pop(fun1<funcontext>);
// 这时ECStack = [globalContext], globalContext永远存在于ECStack
有个很经典的问题:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
答案就是执行上下文的栈不一样。
ECStack.push(checkscope<funcontext>);
ECStack.push(f<funcontext>);
ECStack.pop(f<funcontext>);
ECStack.pop(checkscope<funcontext>);
ECStack.push(checkscope<funcontext>);
ECStack.pop(checkscope<funcontext>);
ECStack.push(f<funcontext>);
ECStack.pop(f<funcontext>);
执行过程
执行分为两个过程,分别是:
-
进入执行上下文
进入执行上下文的时候,并没有执行代码,这个时候变量对象包括:
-
函数的所有形参
-
函数声明
-
变量声明
举个例子:
function foo(a) { var b = 2; function c() {} var d = function() {}; b = 3; } foo(1);
在进入执行上下文以后,长这个样子
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c() {}. d: undefined }
-
-
代码执行
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值,当代码执行玩以后,AO会变成
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: reference to function c() {}. d: reference to FunctionExpression "d" }
让我们来看几个例子
1.第一题
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
上面那个报错,这是因为变量a没有通过a声明,于是在进入执行上下文的时候,AO长这个样子
AO = {
arguments: {
length: 0
}
}
此时AO里面是没有变量a的,于是去全局环境里面找,找了也找不到,于是报错
当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。
2.第二题
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
会打印函数,而不是 undefined 。
这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。