前面的文章介绍了:
- 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 Demo: Statement - Var
- Demystifying JavaScript Variable Scope and Hoisting
- var functionName = function() {} vs function functionName() {}
- Chapter 16. Variables: Scopes, Environments, and Closures
- Block-Scoped Variables
JavaScript data types and data structures JavaScript debugger Statement
(系列完)