js中的变量提升和函数提升

console.log(a);
var a = "a";
var foo = () => {
    console.log(a);
    var a = "a1";
}
foo();

由于js自上而下逐行解释执行的,有人可能会认为第一行代码引用了一个没有声明的变量a,会抛出 ReferenceError  异常,而注掉第一行后,由于变量 a 在第二行log之前已经声明并赋值,打印结果应该是 "a"。而实际的执行结果是:

undefined
undefined

一、什么是提升(Hosting)?

引擎会在解释JavaScript代码之前首先对齐进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。

简单说就是在js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。举例来说:

console.log(a);
var a = 3;
//预编译后的代码结构可以看做如下
var a; // 将变量a的声明提升至最顶端,赋值逻辑不提升。
console.log(a); // undefined
a = 3; // 代码执行到原位置即执行原赋值逻辑

二、变量提升

变量声明的提升是以变量所处的第一层词法作用域为“单位”的,即全局作用域中声明的变量会提升至全局最顶层,函数内声明的变量只会提升至该函数作用域最顶层。那么开始的一段代码经过预编译则变为:

var a;
console.log(a); // undefined
a = "a";
var foo = () => {
    var a; // 全局变量会被局部作用域中的同名变量覆盖
    console.log(a); // undefined
    a = "a1";
}
foo();

输出undefined就很明了。 

ES6新增了let和const关键字,使得js也有了“块”级作用域,而且使用let和const 声明的变量和函数是不存在提升现象的,比较有利于我们养成良好的编程习惯。

三、函数提升

有了上面变量提升的说明,函数提升理解起来就比较容易了,但较之变量提升,函数的提升还是有区别的。举例说明:

console.log(foo1); // [Function: foo1]
foo1(); // foo1
console.log(foo2); // undefined
foo2(); // TypeError: foo2 is not a function
function foo1 () {
	console.log("foo1");
};
var foo2 = function () {
	console.log("foo2");
};

即函数提升只会提升函数声明,而不会提升函数表达式。

再举一个小例子:

var a = 1;
function foo() {
    a = 10;
    console.log(a);
    return;
    function a() {};
}
foo();
console.log(a);

直接上结果:

10
1

 上面的代码块经过预编译后可以看做如下形式(只分析foo方法内部情况):

var a = 1; // 定义一个全局变量 a
function foo() {
    // 首先提升函数声明function a () {}到函数作用域顶端, 然后function a () {}等同于 var a =  function() {};最终形式如下
    var a = function () {}; // 定义局部变量 a 并赋值。
    a = 10; // 修改局部变量 a 的值,并不会影响全局变量 a
    console.log(a); // 打印局部变量 a 的值:10
    return;
}
foo();
console.log(a); // 打印全局变量 a 的值:1

 四、为什么会有提升?

搬运:

下面是Dmitry Soshnikov早些年的twitter,他也对这个问题十分感兴趣, Jeremy Ashkenas想让Brendan Eich聊聊这个话题::

js中的变量提升和函数提升_第1张图片

 Brendan Eich给出了答案: 

js中的变量提升和函数提升_第2张图片

大致的意思就是:由于第一代JS虚拟机中的抽象纰漏导致的,编译器将变量放到了栈槽内并编入索引,然后在(当前作用域的)入口处将变量名绑定到了栈槽内的变量。(注:这里提到的抽象是计算机术语,是对内部发生的更加复杂的事情的一种简化。)

然后,Dmitry Soshnikov又提到了函数提升,他提到了相互递归(就是A函数内会调用到B函数,而B函数也会调用到A函数):

js中的变量提升和函数提升_第3张图片 Brendan Eich又给出答案:js中的变量提升和函数提升_第4张图片

 Brendan Eich很确定的说,函数提升就是为了解决相互递归的问题,大体上可以解决像ML语言这样自下而上的顺序问题。

 最后,Brendan Eich还对变量提升和函数提升做了总结:js中的变量提升和函数提升_第5张图片

大概是说,变量提升是人为实现的问题,而函数提升在当初设计时是有目的的。

 

后记:

  上面有个段落说到的es6之后使用let和const不存在提升是不准确的,可参考: 

   https://stackoverflow.com/questions/31219420/are-variables-declared-with-let-or-const-not-hoisted-in-es6

   大致意思是js中无论哪种形式声明(varletconstfunctionfunction*class)都会存在提升现象,不同的是,  var,function,function*的声明会在提升时进行初始化赋值为 undefined,因此访问这些变量的时候,不会报 ReferenceError 异常,而使用 let,const,class 声明的变量,被提升后不会被初始化,这些变量所处的状态被称为“temporal dead zone”,此时如果访问这些变量会抛出ReferenceError 异常,看上去就像没被提升一样。

 

 

 

 

你可能感兴趣的:(JS-星星之火)