深入浅出JS - 变量提升(函数声明提升)

10,697 阅读3分钟

前言

在我们的日常工作中,变量无处不在。更加深入的去了解它,能够使得自己的JS水平更上一层楼, 从变量提升这个小知识点着手,让我们一起来深入了解JS吧!

变量提升的小栗子

console.log(a) // undefined
var a = 'hello JS' 

/* 在我们声明a之前为什么输出a不会报错呢? 不急,让我们接着往下看 */

num = 6;
num++;
var num;
console.log(num) // 7 好奇怪,为什么给一个还没有声明的变量赋值会不报错呢

function hoistFunction() {
    foo();
    function foo() {        
        console.log('running...')    
    }
}
hoistFunction(); // running... 

/* 最后一个栗子 */

alert(a) //  function a { alert(10) }
a(); // 10
var a = 3;
function a() {    
    alert(10)
};
alert(a) // 3
a = 6;
a(); // throw error


分析原因

JS引擎会在正式执行代码之前进行一次”预编译“,预编译简单理解就是在内存中开辟一些空间,存放一些变量和函数。具体步骤如下(browser):

  • 页面创建GO全局对象(Global Object)对象(window对象)。
  • 加载第一个脚本文件
  • 脚本加载完毕后,进行语法分析。
  • 开始预编译
    • 查找函数声明,作为GO属性,值赋予函数体(函数声明优先)
    • 查找变量声明,作为GO属性,值赋予undefined
    • GO/window = {
          //页面加载创建GO同时,创建了document、navigator、screen等等属性,此处省略
          a: undefined,
          c: undefined,
          b: function(y){
              var x = 1;
              console.log('so easy');
          }
      }
  • 解释执行代码(直到执行函数b,该部分也被叫做词法分析
    • 创建AO活动对象(Active Object)
    • 查找形参和变量声明,值赋予undefined
    • 实参值赋给形参
    • 查找函数声明,值赋给函数体
    • 解释执行函数中的代码

      GO/window = {
          //变量随着执行流得到初始化
          a: 1,
          c: function(){
              //...
          },
          b: function(y){
              var x = 1;
              console.log('so easy');
          }
      }
      
  • 第一个脚本文件执行完毕,加载第二个脚本文件
  • 第二个文件加载完毕后,进行语法分析
  • 开始预编译
    • 重复预编译步骤 ....

预解析机制使得变量提升(Hoisting),从字面上理解就是变量和函数的声明会移动到移动到函数或者全局代码的开头位置。我们再来分析一下小栗子加深一下理解。

console.log(a) // 执行之前,变量提升作为window的属性, 值被设置为undefined
var a = 'hello JS' 

/* JavaScript 仅提升声明,而不提升初始化 */

num = 6;
num++;
var num;
console.log(num) // 变量提升 值为undefined的num赋值为6,再自增 => 7

function hoistFunction() {
    foo();
    function foo() {        
        console.log('running...')    
    }
}
hoistFunction(); // 函数声明提升,可以在函数体之前执行

/* 最后一个栗子 */

alert(a) // 最后的声明为函数声明, 因此a此时为函数体
a(); // 执行 a 函数,输出10
var a = 3; // 3 赋给a
function a() {    
    alert(10)
};
alert(a) // 3
a = 6; // 6赋给a,不是一个函数,故下方执行throw error
a(); // throw error

注: JS并不存在真正的预编译,var与function的提升实际是在语法分析阶段就处理好的。而且JS的预编译是以一个脚本文件为块的。一个脚本文件进行一次预编译,而不是全文编译完成再进行”预编译”的。


最佳实践 

理解了变量提升和函数提升可以使得我们在JS上走的更远,但是我们在开发中,不应该使用这一特性,而是要规范我们的代码,做到可维护性和可读性。无论是变量还是函数,都必须先声明后使用。PS:在开发中应该使用let来约束变量提升。


参考资料:  

https://developer.mozilla.org/zh-CN/docs/Glossary/Hoisting https://www.cnblogs.com/liuhe688/p/5891273.html http://dmitrysoshnikov.com/notes/note-4-two-words-about-hoisting/ https://segmentfault.com/a/1190000010187653