outline

  1. 引子
  2. Scoping in JavaScript
  3. Hoisting in JavaScript
  4. ES6

引子

原文参考https://segmentfault.com/a/1190000003114255

最近在阮一峰的ES6,里面在讲let的时候提到了变量提升,看了下,感觉可以解释清楚不少以前没有理解的东西.
先看个例子

1
2
3
4
5
6
7
8
var foo = 1;
function bar() {
if (!foo) {
var foo = 10;
}
console.log(foo);
}
bar();

有人认为这里!foo是false,那么赋值语句应该不执行.那么输出结果应该是1,however,结果是10!
再来一个例子

1
2
3
4
5
6
7
8
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
console.log(a);

看到这个例子,大家会认为,哎,这里里面的a = 10会覆盖外面的a = 1,觉得答案是10.但是~,答案是1!!
估计这两个例子,可能很多人都会觉得不理解,不理解就对了,往下看!

Scoping in JavaScript

关于JavaScipt中的作用域问题,可以参考我的这篇文章JavaScript没有块级作用域.这里我就提下,在JavaScript中只有两种作用域:全局作用域与function作用域.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
int x = 1;
printf("%d\n", x); // 1
if (1) {
int x = 2;
printf("%d\n", x); // 2
}
printf("%d\n", x); // 1
}

类似于上面的C代码中的依靠{}来形成作用域在JavaScript中是不成立的.来看一段跟上面功能类似的JavaScript代码

1
2
3
4
5
6
7
var x = 1;
console.log(x);//1
if(1){
var x = 2;
console.log(x);//2
}
console.log(x);//2

类似的代码却得不到一样的结果的根源在于在上面的JS代码中,{}var x = 2{}外面的var x = 1;其实是同一个x,{}中修改{}里面的x影响了{}外面的x的.
正如上面所说的JS中只有global和function两种作用域,那么这里,我们也可以通过function作用域来隔离{}内外两个x

1
2
3
4
5
6
7
8
9
var x = 1;
console.log(x);//1
if(1){
(function(){
var x = 2;
console.log(x);//2
})()
}
console.log(x);//1

这里我们用一个function作用域来隔离内外的x,避免内部x赋值对外部的x的影响.

Hoisting in JavaScript

在Javascript中,变量进入一个作用域可以通过下面四种方式:

  1. 语言自定义变量:所有的函数作用域中都存在this和arguments这两个默认变量
  2. 函数形参:函数的形参存在函数作用域中
  3. 函数声明:function foo() {}
  4. 变量定义:var foo

JS代码真正执行过程中,在代码运行前,函数声明和变量定义通常会被解释器移动到其所在作用域的最顶部.
这就是所谓的变量提升,对于var a = 1这句话而言,我们可以拆分成两部分来看变量定义变量赋值.不管var a = 1在作用域的什么地方,变量定义会被提升到到当前作用域的顶部来执行.
举个例子:

1
2
3
4
function foo() {
bar();
var x = 1;
}

对于这段代码JS会这样认为:

1
2
3
4
5
function foo(){
var x;
bar();
x = 1;
}

对于var x = 1,var x被提到bar()之前的过程就是变量提升(hoist),这里要主要提升不仅包含var a这种变量,还包括function a(){};

对于JS中函数定义,我们可以采取function foo(){}或者采取var foo = function(){}(这里我们不谈var foo = new Function()这么烦的方式).这两种方式有什么区别?

1
2
3
4
5
6
7
8
9
10
11
function test() {
foo(); // TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function () { // function expression assigned to local variable 'foo'
console.log("this won't run!");
}
function bar() { // function declaration, given the name 'bar'
console.log("this will run!");
}
}
test();

这里例子等价于

1
2
3
4
5
6
7
8
9
10
11
12
13
function test() {
//hoist variable foo and function bar
var foo; //undefined
function bar() {
console.log("this will run!");
}
foo();
bar();
foo = function () {
console.log("this won't run!");
}
}
test();

从这个等价函数中,我们可以很轻松的看出函数bar被提升,所以后面可以正常的执行.但是var foo = function(){}的方式提升的是foo这个变量,并且初始值为undefined,所以后面执行的时候就相当于执行foo()的时候,就会出现 foo is not a function().
function foo(){}提升的是整个函数定义.
var foo = function(){}提升的是foo变量的定义.

结合JS作用域以及JS变量提升,回头来看之前引子中的例子

1
2
3
4
5
6
7
8
var foo = 1;
function bar() {
if (!foo) {
var foo = 10;
}
console.log(foo);
}
bar();

这里例子等价于

1
2
3
4
5
6
7
8
9
10
var foo;
foo = 1;
function bar(){
var foo;//undefined
if(!foo){
foo = 10;
}
console.log(foo);
}
bar();

在函数bar外面foo为1,但在函数bar内,定义一个新的foo,这个时候foo是undefined,!foo就变成了true,执行赋值操作,foo=10;这个时候输出的foo是函数bar内刚定义的foo,输出的值为10;
另外一个例子:

1
2
3
4
5
6
7
8
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
console.log(a);

等价于

1
2
3
4
5
6
7
8
9
var a;
a = 1;
function b(){
function a(){};
a = 10;
return;
}
b();
console.log(a);

这里函数b的外部定义了a,值为1.函数b内,声明函数a的过程被提升(hoist),之后a被赋值为10;由于是函数b中新定义了a,所以对函数b外部的a并不造成影响,最后得console.log(a)输出的值还是1;

ES6

在ES6中,通过语法糖可以帮助开发者避免一些作用域以及变量提升带来的问题.比如let的使用.这里举两个例子,一个关于变量的,一个关于函数的

ES6中关于变量的例子:

用let替换之前例子中的var

1
2
3
4
5
6
7
8
let foo = 1;
function bar() {
if (!foo) {
let foo = 10;
}
console.log(foo);
}
bar();

babel编译后生成:

1
2
3
4
5
6
7
8
var foo = 1;
function bar() {
if (!foo) {
var _foo = 10;
}
console.log(foo);
}
bar();

执行结果是1;

ES6中关于函数的例子:

1
2
3
4
5
6
function f() { console.log('I am outside!'); }
if(true) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();

ES5执行结果是I am inside!,两次function f(){}的声明都被提升了,赋值取最后一次赋值.所以执行结果是I am inside!
ES6以babel编译为例,编译出来结果是

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

function f() {
console.log('I am outside!');
}
if (true) {
// 重复声明一次函数f

var _f = function _f() {
console.log('I am inside!');
};
}
f();

babel编译自动把里面同名函数f变成了_f.执行结果是I am outside!