1、首先看3个例子:
console.log( a ); //会打印出什么呢?
var a = 1;
b = 2;
var b;
console.log( b ); //会打印出什么呢?
foo(); //会打印出什么呢?
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
如果你对以上例子的执行原理还不太熟悉,不要紧,跟着本文的介绍,你会对此有所掌握的。(对结论感兴趣的可以直接跳到本文最后总结)
2、变量的提升
众所周知 js 的代码执行顺序一般是从上而下的一条条执行。但是在进行变量申明和函数申明时,会有提升的情况。接下来我们看看 js 是如何提升变量和函数声明的。
js 代码在被编译器编译时,会经过3个阶段:词法分析、语法分析、代码生成;变量的提升发生在语法分析阶段。
当我们在申明一个变量时,比如 var a = 1;
我们习惯将 var a = 1; 看作一个声明, 而实际上 JavaScript 引擎并不这么认为。 它将 var a 和 a = 1 当作两个单独的声明, 第一个是编译阶段的任务, 而第二个则是执行阶段的任务,具体如下:
1)首先,会进行词法分析,找出哪些词是代码语句,上例申明会被拆分为:var、a、=、1、; 这5个词法单元(可能我们会觉得这不是废话吗,其实这个过程是找出那些正确的代码词法单元,以便后续的分析,毕竟有时开发者在书写 js 代码时会有不严格的情况,比如有的开发者忘记分号、多余或少空格等等之类不规范)
2)其次,会进行语法分析,组装词法单元并分析,会将词法单元流(数组)转换成一个由元素逐级嵌套而成的“抽象语法树”,上例 var a = 1,js 引擎会将其分解成2个申明:
var a;
a = 1;
编译器在遇到 var a 时,编译器在词法分析时会询问作用域内是否已经存在一名名字叫做 a 的变量,如果是,则编译器会忽略该声明继续进行编译,否则它会在当前作用域的集合内申明一个新的变量,并命名为 a,又因为每个作用域集合是在该作用域代码的运行时最开始执行的,所以就会出现 变量声明 被提前的情形。
3)最后,会进行代码生成,编译器会为 js 引擎生成运行时所需的代码,这些代码被用来处理 a = 1 这个赋值操作。
综上,所以第一个例子:
console.log( a );
var a = 1;
实际执行流程是:
var a;
console.log( a ); //会打印出 undefined
a = 1;
第二个例子:
b = 2;
var b;
console.log( b );
实际执行流程是:
var b;
b = 2;
console.log( b ); //会打印出 2
3、关于函数的提升
函数声明跟变量声明一样会被提升。
总结:
1、无论作用域中的声明出现在什么地方, 都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数) 都会被“移动” 到各自作用域的最顶端, 这个过程被称为提升。
2、变量在申明时,会出现提升,只要是 var 一个变量,该变量申明会被提升到当前作用域的最开始部分。
3、函数在申明时,会出现提升,该函数申明会被提升到当前作用域的最开始部分。
4、另外值得注意的是,每个作用域都会进行提升操作,比如:
foo();
function foo() {
console.log( a ); // undefined
var a = 1;
}
实际执行流程是:
function foo() {
var a;
console.log( a ); // undefined
a = 1;
}
foo();
PS:本文不讨论 ES6 的 let 申请变量的方式带来的改变(比如,没有变量提升、不可重复申明变量、作用域只在它申明时所在的代码块内有效)
(完)
想要打赏,请点击这里