声明提升(Hoisting)
聊到声明提升就必须聊一下浏览器对于 JavaScript 代码的解析步骤
三个参与者:编译器、作用域、引擎
编译器会对源代码进行解析,生成可供浏览器执行的代码
在解析完成之后,引擎会执行解析后的代码
作用域参与了这两个环节,编译过程中对于需要进行变量提升的变量,会先将这些变量存储在作用域中,然后在引擎执行的过程中对其进行赋值操作
定义
在 同一个 script 标签内,在 ES5 中,在进入 全局作用域 或者 函数作用域 之前,会对作用域的代码先执行编译,然后再执行
这是什么意思呢?在 ES5 中,可以形成块级作用域的只有函数,所以 JavaScript 的解析也是非常的简单粗暴。在编译过程中,遇到了 var,则将变量名保存在作用域中,遇到了函数声明,则将整个函数块 function (arguments) {...} 提升到代码顶部
而在每次执行函数前,都会对于函数内的代码进行编译,然后再执行
遇到了 var,则将变量名保存在作用域中,遇到了函数声明,则将整个函数块 function (arguments) {...} 提升到代码顶部
关于定义中的其他限定条件下面再聊,先来聊聊主体部分
实例 1
console.log(a); // undefined var a = 1;
这里代码的执行步骤可以用下面来表示
var a; console.log(a); a = 1;
由于碰到了 var 关键字,这里会对其进行声明提升,所以这里会打印出 undefined
实例 2
console.log(fn); // 1
function fn () {
return 1;
};
这里同样也是,会对函数声明进行变量提升
function fn () {...};
console.log(fn());
这里会先将整个函数块提升至代码顶部,并不会去管函数内有啥,然后执行代码,遇到 console.log(fn()) 直接执行函数并打印出结果
实例 3
var fn = function () {
return 1;
};
console.log(fn); // undefined
可以将编译和执行过程表示成如下
var fn;
console.log(fn);
fn = function () {
return 1;
};
这里一定要弄清楚函数表达式和函数声明的区别,不然会很容易犯错
在同一个 script 标签内
为什么这里要说同一个 script 标签呢?虽然这里多个 script 标签共享作用域,但是它们却是编译并执行完一个 script 标签内的代码,然后才会对下一个 script 标签内的代码进行编译和执行
-
先来测试一下,多个 script 标签是否公用一个作用域:
<script> var a = 1; </script> <script> console.log(a); // 1 </script>可以看到这里输出了 a 的值,说明多个 script 标签公用一个作用域
-
再来测试一下多个 script 标签的编译和执行顺序:
同一个 script 标签内
function fn () { console.log(1); } function fn () { console.log(2); }; fn(); // 2可以看到浏览器只打印了 2
<script> function fn () { console.log(1); }; fn(); // 1 </script> <script> function fn () { console.log(2); }; fn(); // 2 </script>这里会先打印出 1,然后再打印出 2,如果是先编译第一个 script 标签内的代码,然后再编译下一个 script 标签内的代码,则会和上面的代码结果相同,但是这里有两个结果,说明虽然多个 script 标签是公用一个作用域,但是它们却是先执行完一个标签内的再执行下一个
-
所以这里定义前面加一条限制,在同一个 script 标签内,然后再进入一个作用域之前会对代码进行预解析
<script> console.log(a); // ReferenceError: a is not defined </script> <script> var a = 1; </script>这里结果就很明显了,在执行第一个 script 标签内的代码时并不能获取到 a,所以这里会报错
在 ES5 中,在进入一个作用域之前
在全局作用域代码编译完后,代码执行的过程中,遇到函数执行语句,都会对其内的代码先进行编译,再执行
var a = 1;
function fn () {
console.log(a); // undefined
var a = 2;
};
这是一段误导性很强的代码,根据作用域链的规则,都是在当前作用域去查找所需要的变量,如果未找到,则会沿着作用域链按顺序向上查找。按理说,不管是在函数内部还是在外部都声明了变量 a,并且进行了赋值,而在函数内部的打印结果却是 undefined,这就是声明提升导致的结果
我们可以根据声明提升的规则来分析上述代码编译和执行步骤:
全局会进行一次编译,将 var 声明的变量和函数声明提升到代码顶部
var a;
function fn () {...};
然后开始执行,遇到了变量 a 的赋值操作
a = 1;
接着开始执行函数,这时要先对函数内部的代码进行编译,函数内部有一个变量 a 通过 var 关键字声明,此时就要进行声明提升
function fn () {
var a;
...
};
然后执行
function fn () {
var a;
console.log(a);
a = 2;
};
所以这里最后会输出 2
所以定义里也提到了 每次执行函数前,都会对于函数内的代码进行编译,然后再执行
规则
- 函数声明的声明提升优先于
var关键字带来的声明提升 - 编译过程中,编译器碰到了
var关键字,会先问作用域中是否含有该变量,如果有,则会忽略这个var然后继续执行编译,如果没有,则会为其分配内存并保存在作用域中
声明提升顺序
函数声明提升优先于
var关键字带来的变量提升
其实这里纠结于提升顺序并没有任何意义,因为不管谁先提升,最终的结果都会一样
看官们别急,我们来看看下一条规则
查询作用域
console.log(fn); // function fn () { retrn 1 }
function fn () {
return 1;
};
var fn = function () {
return 2;
};
我们来对上述代码进行分析
function fn () {
return 1;
};
// var fn;
console.log(fn);
fn = function () {
return 2;
};
可以看到这里输出的结果是函数声明,可能会认为这里是变量提升优先于函数声明提升
但是第二条规则里面也说了,在遇到 var 关键字,会先去作用域里查找是否含有该变量,如果有,则会忽略这个 var 然后继续执行编译
所以说不管是谁先提升,如果 var 关键字声明的变量与函数声明相同,那么这个 var 关键字的声明语句会直接被忽略,而赋值操作永远是在提升之后,所以不管是谁先提升,结果总是相同的
故这里有个猜想
在函数声明 function fn () {} 的提升过程中,首先是会声明一个变量,名为 fn,然后创建函数对象,并将 fn 的引用指向该函数对象
而对于 var 关键字声明的函数,则只会将变量的声明提升,函数体还留在原处,赋值操作只会在代码执行的过程中进行
填个坑(历史遗留问题以及 ES6 中的函数声明)
console.log(a); // undefined
if (false) {
var a = 1;
};
console.log(a); // undefined console.log(a); // undefined
if (false) {
function a () {
return 1;
};
};
console.log(a); // undefined console.log(a); // undefined
if (true) {
var a = 1;
};
console.log(a); // 1 console.log(a); // undefined
if (true) {
function a () {
return 1;
};
};
console.log(a); // function a () { return 1 }
在 ES5 中,规范规定函数只能在 全局作用域 和 函数作用域 之中声明
但是实际上各大浏览器出于兼容性的考虑,都没有遵守这个规范
这里真正的问题在于函数声明是在 if 语句内部的,if 语句以及其他语句创造的代码块内 ({}) 创造的函数声明是否会提升没有一致性,每个浏览器都有自己的行为,所以也是要求不要在代码块内 ({})声明和定义函数
在 ES6 中,多出了块级作用域的定义,对于在块级作用域中定义的函数,也会存在声明提升,只是提升的规则变化了
在支持 ES6 的浏览器中,它们的行为实际上是这样的:
- 允许块级作用域中定义函数
- 函数声明实际上将会类似于利用
var关键字声明的函数表达式,会将变量名提升至顶部,而函数语句则会在执行中被指向变量
但是这种行为避免不了会产生一些不可预估的问题,所以最佳的实践方式还是不要在 全局作用域 和 函数作用域 以外的地方声明函数
总结一下
- 声明提升(Hoisting)发生在
var关键字 和 函数声明 身上 - 函数声明的提升优先级高于
var关键字,但是它们优先级高低与否并不会影响代码的执行结果 - 遵守规范,不要在 全局作用域 以及 函数作用域 外声明函数!