(function() {
console.log(typeof foo); // 这里会打印出什么?
console.log(typeof bar); // 这里会打印出什么?
var foo = 'hello',
bar = function() {
return 'vian';
};
function foo() {
return 'hello';
}
})();
复制代码
咱们在接触JavaScript这门语言时,会常常遇到这种问题,通过后续的学习,咱们可能知道了这种现象在JavaScript中叫声明提高(hoisting),可是咱们可能只知道声明提高的现象,却不清楚形成这种现象的本质,而这个本质倒是JavaScript最为重要的知识之一。解答上述代码中的问题以前,咱们先看一下JavaScript中的执行环境。es6
执行环境(execution context)定义了变量或函数有权访问的其余数据,决定了它们各自的行为。每一个执行环境都有一个与之关联的变量对象(variable object),环境中定义的全部变量和函数都保存在这个对象中。虽然咱们编写的代码没法访问这个对象,但解析器在处理数据时会在后台使用它。浏览器
在JavaScript中可执行代码能够分为三类:bash
全局执行环境是最外围的一个执行环境。在Web浏览器中,全局执行环境是window对象。当代码载入浏览器时,全局执行环境被建立,直到网页或浏览器被关闭,全局执行环境才被销毁。 在一个Javascript程序中,必须且只能存在一个全局执行环境,和任意个的非全局执行环境,程序每调用一个函数,都会建立新的执行环境,以下图所示。函数
浏览器中JavaScript是单线程的,这意味着运行在浏览器中的JavaScript代码,同一时间只有一件事件、动做发生。除了当前正在运行的代码,其余代码都在一个队列中排队,这个队列就是执行环境栈。 为了更好的说明执行环境栈,咱们结合一个例子来讲明:学习
(function test(i) {
if (i === 1) {
return;
}
test(++i);
})(0);
复制代码
下面咱们用图来表示上述代码执行时,执行环境栈中的变化过程: ui
当JavaScript代码执行时,第一个建立的老是全局执行环境,所以全局执行环境也老是在执行环境栈的最底部。 this
代码执行时,调用了函数test(0),此时建立新的执行环境test EC,并压入执行栈。 spa
在执行第一次执行函数test(i)时(i=0),执行到函数体中的代码test(++i),程序再一次调用函数test(i),建立新的执行环境test EC1,并压入执行栈。 线程
函数test(i = 1)中的代码执行完毕,该执行环境出栈销毁,程序回到上一层执行环境中继续执行。 code
5.函数test(i = 0)中的代码执行完毕,该执行环境出栈销毁,程序回到全局执行环境中继续执行,全局环境的代码即便执行完毕,也不会销毁,直至网页或浏览器被关闭了才出栈销毁。
上面就是程序执行过程当中,执行环境栈的变成过程。关于执行环境栈,有如下几点要特别注意:
从上文咱们已经知道了,每当一个函数被调用时,一个新的执行环境就会被建立。实际上,执行环境能够分为两个阶段,建立阶段和执行阶段。JavaScript声明提高的秘密也在其中,咱们继续往下看。
咱们彻底能够把执行环境看成一个含有三个属性的对象,以下:
executionContextObj = {
'variableObject': {...}, //函数的arguments、参数、函数内的变量及函数声明
'scopeChian': {...}, //本层变量对象及全部上层执行环境的变量对象
'this': {}
}
复制代码
声明提高的秘密就发生在变量对象VO中。
每一个执行环境都有一个与之关联的变量对象,环境中定义的全部变量和函数都保存在这个对象中。
说到执行环境的建立过程就会涉及到变量对象和活动对象,不少人对这两个概念会模糊不清。其实,变量对象VO和活动对象AO是同一个对象在不一样阶段的表现形式。当进入执行环境的创捷阶段时,变量对象被建立,这时变量对象的属性没法被访问。进入执行阶段后,变量对象被激活变成活动对象,此时活动对象的属性能够被访问。 下面来看一下,执行环境建立阶段中变量对象建立中,JavaScript解析器作的事情:
JavaScript中声明提高的背后缘由已经很清晰了,你发现了吗?请先思考一下,咱们下文将结合例子进行讲解。先让咱们结合一段代码,结合上文的知识,回顾一下代码执行时,会发生什么事情。
function greet(name) {
var say = 'hello';
function action() {
console.log(say + name);
}
action();
}
greet('vian');
复制代码
executionContextObj = {
arguments: {0: 'vian', length: 1},
name: 'vian'
}
复制代码
executionContextObj = {
arguments: {0: 'vian', length: 1},
name: 'vian',
action: <action>
}
复制代码
executionContextObj = {
arguments: {0: 'vian', length: 1},
name: 'vian',
action: <action>,
say: undefined
}
复制代码
经过对JavaScript中执行环境的了解,使人奇怪的声明提高机制也变得清晰明了。回到本文的开头,形成这种声明提高现象的本质,到底是什么呢?——执行环境的建立阶段,变量对象建立的方式所形成。下面咱们来解释一下本文开头的代码。
(function() {
console.log(typeof foo); // 这里会打印出什么?
console.log(typeof bar); // 这里会打印出什么?
var foo = 'hello',
bar = function() {
return 'vian';
};
function foo() {
return 'hello';
}
})();
复制代码
根据上文中分析变量对象建立过程的方法:
executionContextObj = {}
1.初始化arguments对象,及形参
executionContextObj = {
arguments: {length: 0}
}
2.扫描函数声明,并进行处理:
遇到函数声明 function foo(){}
executionContextObj中没有foo属性,将foo设为executionContextObj的属性,函数引用做为值。
executionContextObj = {
arguments: {length: 0},
foo: <function>
}
3.扫描变量声明,并进行处理:
遇到变量声明var foo,executionContextObj已存在foo属性,跳过。
遇到变量声明var bar,executionContextObj不存在bar属性,将其设置为变量对象属性,值为undefined。
executionContextObj = {
arguments: {length: 0},
foo: <function>,
bar: undefined
}
复制代码
结论:
console.log(typeof foo); // 'function'
console.log(typeof bar); // 'undefined'
复制代码
到这里,咱们就不再怕声明提高的坑和问题啦。须要注意的是,es6中新增的let和const变量声明都不会进行变量提高,重复赋值及声明前引用变量都会报错(TDZ)。
var name = 'vian';
if (1) {
name = 'em'; //TDZ开始 ReferenceError: Cannot access 'name' before initialization
let name; //TDZ结束 name值不会被'em'覆盖,由于块级做用域中使用了let声明name,此时name
//绑定在了块级做用域中,且不受外部影响。
}
复制代码
到了ES6,使用新增的let、const关键字声明的变量,不会按照上文的规则进行变量提高。ES6明确规定,若是区块中存在let和const关键字,区块对这些这些声明的变量,从一开始就造成一个封闭的做用域,这些变量再也不受外部的影响(可理解为这些变量绑定在区块上)。在声明以前使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量以前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)
但愿本文对你们有所帮助,互相学习,一块儿提升。转载请注明原帖。