不少编程语言在执行的时候都是自上而下执行,但实际上这种想法在JavaScript中并不彻底正确, 有一种特殊状况会致使这个假设是错误的。来看看下面的代码,编程
a = 2; var a; console.log( a );
console.log(a) 会输出什么呢?编程语言
有些人可能会认为是 undefined,由于 var a 声明在 a = 2 以后,他们天然而然地认为变量被从新赋值了,所以会被赋予默认值 undefined。可是,真正的输出结果是 2。 函数
先不急为何,咱们再继续看另一段代码,spa
console.log( a ); var a = 2;
鉴于上一个代码片断所表现出来的某种非自上而下的行为特色,你可能会认为这个代码片断也会有一样的行为而输出 2。还有人可能会认为,因为变量 a 在使用前没有先进行声明,所以会抛出 ReferenceError 异常。code
其实否则,两种猜想都是不对的。输出来的会是 undefined。
blog
引擎会在解释 JavaScript 代码以前首先对其进行编译,简单地说,任何 JavaScript 代码片断在执行前都要进行编译(一般就在执行前,说一般是由于JavaScript 中存在两个机制能够“欺骗” 词法做用域: eval(..) 和 with)。编译阶段中的一部分工做就是找到全部的声明,并用合适的做用域将它们关联起来,包括变量和函数在内的全部声明都会在任何代码被执行前首先被处理。这就是咱们一般说的“提高”。
注:只有声明自己会被提高, 而赋值或其余运行逻辑会留在原地。
ip
foo(); function foo() { console.log( a ); // undefined var a = 2; }
每一个做用域都会进行提高操做。因此 foo(..)函数自身也会在内部对 var a 进行提高(显然并非提高到了整个程序的最上方)。在这里,你或许会发现,为何代码里面是先调用 foo() ,再声明 foo() 这样的顺序,却不会报错。这是由于除了变量声明会在其做用域内提高以外,函数声明也具备类似的特效。所以这段代码能够暂时理解为下面的形式:作用域
function foo() { var a; console.log( a ); // undefined a = 2; } foo();
能够看到,函数声明会被提高在做用域的顶部。可是有一点须要和变量声明提高作区别的是:变量提高只是提高了变量的声明,而变量赋值并无被提高。可是,函数的声明有点不同,函数体也会一同被提高。开发
因此上面的一段暂时性的代码实际上能够这样理解:it
var foo = { var a; console.log( a ); // undefined a = 2; } foo();
foo 函数的声明(这个例子还包括实际函数的隐含值)被提高了,所以第一行中的调用能够正常执行。
然而并非全部的函数都能提高!函数声明会被提高,可是函数表达式却不会被提高。
foo(); // 不是 ReferenceError, 而是 TypeError! var foo = function bar() { // ... };
上面这段程序中的变量标识符 foo() 被提高并分配给所在做用域,所以 foo() 不会致使 ReferenceError。可是 foo 此时并无赋值(若是它是一个函数声明而不是函数表达式,那么就会赋值)。foo() 因为对 undefined 值进行函数调用而致使非法操做,所以抛出 TypeError 异常。
同时也要记住,即便是具名的函数表达式,名称标识符在赋值以前也没法在所在做用域中使用:
foo(); // TypeError bar(); // ReferenceError var foo = function bar() { // ... };
这个代码片断通过提高后,实际上会被理解为如下形式:
var foo; foo(); // TypeError bar(); // ReferenceError foo = function() { var bar = ...self... // ... }
这里咱们说到具名函数表达式,就顺便插如一点具名函数表达式的知识点。咱们看看下面的例子:
function test() { var fn = function fn1() { log(fn === fn1); // true log(fn == fn1); // true } fn(); log(fn === fn1); // Uncaught ReferenceError: fn1 is not defined log(fn == fn1); // Uncaught ReferenceError: fn1 is not defined } test();
看上面这例子,是否是很疑惑?
具名函数表达式,是带名字的函数赋值给一个变量,这个名字只在新定义的函数做用域内有效,由于规范规定了标示符不能在外围的做用域内有效。也就是说,这个函数名只能在此函数内部使用,能够理解为这个函数名成了函数体内部的一个变量。
这里还有一点须要注意的,函数定义了一个非标准的name属性,经过这个属性能够访问到给定函数指定的名字,这个属性的值永远等于跟在function关键字后面的标识符,匿名函数的name属性为空,而具名的函数表达式会修改到这个属性。
var foo = function(){ //... }; console.log(foo.name); //foo var bar = function foobar(){ //... }; console.log(bar.name); //foobar name值被修改
函数声明和变量声明都会被提高。可是一个值得注意的细节(这个细节能够出如今有多个“重复” 声明的代码中)是函数会首先被提高,而后才是变量。
看一下下面的代码:
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
会输出 1 而不是 2 ! 这个代码片断会被引擎理解为以下形式:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
var foo 尽管出如今 function foo()... 的声明以前,但它是重复的声明(所以被忽略了),由于函数声明会被提高到普通变量以前。尽管重复的 var 声明会被忽略掉, 但出如今后面的函数声明仍是能够覆盖前面的。
foo(); // 3 function foo() { console.log( 1 ); } var foo = function() { console.log( 2 ); }; function foo() { console.log( 3 ); }
咱们来看看下面这个,
function text1() { var a = 1; function b() { a = 10; return; function a() {} } b(); console.log(a); // ? } text1(); function text2() { var a = 1; function b() { a = 10; function a() {} } b(); console.log(a); // ? } text2();
想想,这两段代码输出的结果会是什么?
结果都是1!为啥???
这里须要注意的是,在 function b() 中,function a() 因为存在函数提高,上述代码实际上的运行代码是这样子的,
function text{ var a = 1; function b() { var a = function(){}; a = 10; //return; //这个return对这段代码没有任何影响 } b(); console.log(a); 1 }
是否是很神奇~~~~因此在写代码的时候,就要特别注意了,不要由于 JavaScript 的提高机制致使不少莫名其妙的bug出来。
最后还有一个要强调一下,因为一个普通块内部的函数声明一般会被提高到所在做用域的顶部,这个过程不会像下面的代码暗示的那样能够被条件判断所控制:
foo(); // "b" var a = true; if (a) { function foo() { console.log("a"); } } else { function foo() { console.log("b"); } }
function hoistVariable() { if (!foo) { var foo = 5; } console.log(foo); // 5 } hoistVariable();
咱们习惯将 var a = 2; 看做一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a和 a = 2 看成两个单独的声明, 第一个是编译阶段的任务,而第二个则是执行阶段的任务。这意味着不管做用域中的声明出如今什么地方,都将在代码自己被执行前首先进行处理。能够将这个过程形象地想象成全部的声明(变量和函数)都会被“移动”到各自做用域的最顶端,这个过程被称为提高。
声明自己会被提高,而包括函数表达式的赋值在内的赋值操做并不会提高。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一块儿的时候,不然会引发不少危险的问题!
理解变量提高和函数提高可使咱们更了解这门语言,更好地驾驭它,可是在开发中,咱们不该该使用这些技巧,而是要规范咱们的代码,作到可读性和可维护性。具体的作法是:不管变量仍是函数,都必须先声明后使用。
若是对于新的项目,可使用let替换var,会变得更可靠,可维护性更高。值得一提的是,ES6中的class声明也存在提高,不过它和let、const同样,被约束和限制了,其规定,若是再声明位置以前引用,则是不合法的,会抛出一个异常。
因此,不管是早期的代码,仍是ES6中的代码,咱们都须要遵循一点,先声明,后使用。