前面的文章介绍了:

  • var声明的变量是放置在局部作用域中
  • 函数调用时会产生一个临时对象,局部作用域中的变量实际对应着这个临时对象的属性

这篇文章要介绍的是:

  • 由于在函数体执行前,这个临时对象就已经创建了,所以var声明的变量也就随着这个临时对象的创建而创建了

hoisting:先使用后定义

下面的例子都在浏览器环境中执行

让我们来看一个例子:

function f() {
        msg = "hello";
        var msg;
        console.log(msg);
}

f(); // 输出 hello

上面的代码,按照以前的理解,msg = "hello"不是应该创建一个全局的变量msg吗?然后var msg创建一个局部的msg变量,值为空。最后的console.log(msg)应该引用的是值为空的局部的msg变量,而不是全局的值为"hello"的msg变量才对?

实际上,根据文章开头的解释,var msg;申明的变量,对应着函数体执行前就创建好了的临时对象的属性。所以在msg = "hello"执行的时候,由于临时对象已经存在,所以msg变量所对应的临时对象的属性也存在了。所以msg = "hello"中的msg变量和随后的var msg;中的msg变量其实是一个东西,都对应着当前临时对象的同一个属性。

上面的这个现象叫做hoisting,即变量的定义可以出现在变量的使用之后。

函数的hoisting

函数同样支持hoisting,看下面的例子:

function f() {
        hi()

        function hi() {
                console.log("hello")
        }
}

f(); // 输出 hello

同样的,hi()的调用是在function hi()之前,但是能获得正确的结果。

函数优先还是变量优先

既然函数定义和变量定义都可以hoisting,那么哪个优先呢?下面的例子,定义了一个同名的变量和函数,看看输出结果:

function f() {
        hi();

        var hi = function() {
                console.log("hello, var!");
        }

        function hi() {
                console.log("hello, fun!");
        }
}

f(); // 输出 hello, fun!

如果是变量定义优先的话,上面应该输出hello, var!,可是结果却是 hello, fun!,可见是函数定义优先。

稍微调整一下上面的例子,在函数结尾调用hi()

function f() {
        hi();

        var hi = function() {
                console.log("hello, var!");
        }

        function hi() {
                console.log("hello, fun!");
        }

        hi(); // 新增加的语句
}

f(); // 输出 hello, fun!
     //      hello, var!

上面的例子可以看出,一开始确实函数定义优先,但是执行完var hi = function() {...}之后,函数的定义被替换掉了,所以第二次调用hi()的结果是hello, var!

如何阻止hoisting

定义函数和变量时产生的hoisting行为有时候会产生意想不到的结果,本章讲述的几种办法可以显示地避免这些行为。

IIFE

IIFE是Immediately invoked function expression的缩写。它的原理很简单,既然局部作用域的范围是函数调用时产生的临时对象的属性集合,那么为了控制局部作用域的范围,索性在需要的地方进行函数调用好了,比如下面这个例子:

function f() {
        (function() {
                var msg = "hello, IIFE!";
                console.log(msg);
        }());

        console.log(typeof(msg));
}

f(); // 输出:hello, IIFE!
     //      undefined

从上面的例子可以看出,msg的范围被限定在(function() {...}());这个函数调用中了。

IIFE的常用写法是:

(function() {
        ...
}());

之所以搞这么复杂是为了避免JavaScript把上面的语句当成函数定义,因为IIFE是一个必须立即执行的表达式。根据Speaking JS ,IIFE还有其他写法,比如:

!function () {
        ...
}();

或者

void function () {
        ...
}();

ES6的let和const

新版的JavaScript引入了两个关键字,分别是let和const。有了这两个关键字,JavaScript终于有了真正的局部作用域,或者叫做块作用域:

function f() {
        for (let i=0; i<3; i++) {
                console.log(i);
        }
        console.log(typeof(i));
}

f()

/*
输出:
0
1
2
undefined
*/

如果用var替换上面的let的话,在for循环结束的时候i的值为3.

function f() {
        console.log(typeof(pi));
        const pi = 3.1415;
}

f()

/*
错误输出为:Uncaught ReferenceError: pi is not defined
*/

其他参考

JavaScript data types and data structures JavaScript debugger Statement

(系列完)