经过以前的文章,咱们熟悉了做用域的基本概念。可是做用域中的变量,函数声明在什么地方查找,引用它们的时候又发生了什么。正是咱们将要讨论的内容。前端
在咱们的认知中JavaScript
代码在执行的时候是由上到下一行一行执行的。但实际上并不彻底正确。例如:编程
a = 1;
var a;
console.log(a);
复制代码
按照咱们以前的认知由上到下,最后a
输出undefined
,由于var a
声明在a = 1
后面,但最后输出的结果是1
。数组
考虑另一段代码:函数
console.log(a);
var a = 1;
复制代码
鉴于上一个代码片断所表现的特色,可能认为这个代码片断也会输出1
,或者可能抛出异常错误。实际上输出的是undefined
。ui
那么究竟是声明在前,仍是赋值在前?this
为了弄明白这个问题,咱们须要再次回顾JavaScript
引擎,引擎会在解释JavaScript
代码以前首先对其进行编译。编译阶段中的一个很重要的工做就是找到全部的声明,并在合适的做用域中将它们关联起来。spa
执行环境也能够叫执行上下文,每当JavaScript
编译器工做时,都会建立一个执行环境或者说进入一个执行上下文中。它们定义了变量或函数访问其余数据的权限,决定了它们各自的行为。它们在逻辑上组成一个堆栈,堆栈底部永远是全局环境,而顶部就是当前环境。设计
例如:咱们能够定义执行环境是一个数组:3d
stack = [];
复制代码
在初始化阶段,stack
是这样的:指针
stack = [
globalContext
];
复制代码
每次函数执行,进入function
的时候,这个堆栈都会被压入。
function foo(){
return 'hello';
}
foo();
复制代码
那么,stack
将会发生改变:
stack = [
<foo> functionContext globalContext ]; 复制代码
每一个函数都有本身的执行环境,每次函数退出也就是执行到return
的时候,都会退出当前的执行环境,相应的stack
就会弹出,栈中的指针会移动位置。相关代码执行完毕后,stack
只会包含全局环境,一直到整个程序结束。
在进行JavaScript
编程是总避免不了声明函数和变量,在每一个执行环境中有一个变量对象,咱们定义的全部变量和函数都保存在这个对象中。
变量对象(VO)存储一下内容:
函数声明(function)
变量声明(var)
咱们能够用一个JavaScript
对象来表示一个变量对象例如:
VO = {};
复制代码
如前面所说执行环境中有一个变量对象,它是执行环境的一个属性,例如:
context = {
VO = {};
}
复制代码
当咱们声明一个变量或一个函数的时候,例如:
var a = 1;
function foo() {
var b = 20;
};
test();
复制代码
对应的变量对象是:
//全局环境的变量对象
globalContext: {
vo: {
a: 1,
foo: function } } //foo函数环境的变量对象 fooContext: {
vo: {
b: 20
}
}
复制代码
抽象变量对象VO
全局执行环境变量对象GlobalContextVO (VO === this === global)
函数执行环境变量对象FunctionContextVO (VO === AO, 而且添加了arguments)
全局对象是在进入任何执行环境以前就已经建立了的对象;这个对象只存在一份,它的属性在程序中任何地方均可以访问,全局对象的生命周期终止于程序退出那一刻。
global = {
Math: <...>, String: <...> ... ... window: global //引用自身 }; var a = 1; console.log(a); // 直接访问,在VO(globalContext)里找到:a console.log(window['a']); // 间接经过global访问:global === VO(globalContext): a console.log(a === this.a); // true var b = 'b'; console.log(window[b]); // 间接经过动态属性名称访问:b 复制代码
在函数执行环境中,变量对象是不能直接访问的,此时由活动对象(AO)代替变量对象。活动对象是在进入函数执行环境时被建立的,它经过函数的arguments属性初始化。
VO(functionContext) === AO;
AO = {
arguments: <Args> }; // Arguments对象是活动对象,属性以下: // callee — 指向当前函数的引用 // length — 真正传递的参数个数 function foo(a, b, c) { // 声明的函数参数数量arguments (a, b, c) console.log(foo.length); // 3 // 真正传进来的参数个数(only x, y) console.log(arguments.length); // 2 // 参数的callee是函数自身 console.log(arguments.callee === foo); // true // 参数共享 console.log(a === arguments[0]); // true console.log(a); // 10 arguments[0] = 20; console.log(a); // 20 a = 30; console.log(arguments[0]); // 30 // 不过,没有传进来的参数c,和参数的第3个索引值是不共享的 c = 40; console.log(arguments[2]); // undefined arguments[2] = 50; console.log(c); // 40 } foo(10, 20); 复制代码
当代码在一个执行环境中,引擎会建立变量对象的一个做用域链。做用域链的用途是保证对执行环境有权限的变量和函数的有序访问。做用域链的前端,始终都是当前执行的代码所在环境的变量对象。若是这个环境是函数,则活动对象(VO)做为变量对象(即arguments
对象)。做用域链中的下一个变量对象来自包含环境,而再下一个变量对象则来自下一个包含环境,一直延续到全局执行环境;全局执行环境的变量对象始终都是做用域链中的最后一个对象。
做用域链与一个执行环境相关,变量对象的做用域链用于在标识符解析中变量查找。
函数执行环境的做用域链在函数调用时建立的,包含活动对象和这个函数内部的[[scope]]属性,例如:
var a = 1;
function foo() {
var b = 2;
console.log(a + b);
}
foo(); // 3
复制代码
函数建立时:
fooContext.AO = {
b: undefined // undefined – 进入上下文的时候是2 – 活动对象
};
复制代码
函数foo
如何访问到变量a
?理论上函数应该能访问一个更高一层执行环境的变量对象。实际上它正是这样,这种机制是经过函数内部的[[scope]]属性来实现的。[[scope]]是全部父变量对象的层级链,处于当前函数执行环境之上,在函数建立时存于其中,直至函数销毁。函数foo
的[[scope]]以下:
foo.[[Scope]] = [
globalContext.VO // === Global
];
复制代码
函数调用时:
Scope = AO|VO + foo.[[Scope]]
复制代码
进入foo
执行环境建立AO/VO以后,在执行环境中建立一个做用域链(Scope属性)。这样foo
函数就能访问全局执行环境中的变量a
。
经过前面对于引擎的了解,变量提高是Javascript
中执行环境和变量对象的工做方式的一种认知。它处于代码的编译阶段,JavaScript
仅提高声明,而不提高初始化。
当咱们的代码运行时,首先在执行环境的变量对象中声明变量和函数,而后才是代码执行阶段。当咱们看到var a = 1
时,实际上JavaScript
会将其当作两部分:var = a
和a = 1
。
var a;
a = 1;
//全局环境的变量对象
globalContext: {
vo: {
a: 1
}
}
复制代码
函数声明会首先被提高,而后才是变量,例如:
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
复制代码