提升

# 提升

  1. 任何声明在某个作用域内的变量,都将附属于这个作用域。
  2. 包括变量和函数在内的所有声明都会在任何代码被执行前首先在编译阶段被处理。
  3. 我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。

[TOC]

# 一、函数提升

# 1.1 函数声明会被提升,但函数表达式却不会被提升。

foo(); // 不是ReferenseError,而是TypeError!

var foo = function bar () {
    // ...
}
1
2
3
4
5

这段程序中的变量标识符 foo() 被提升并分配给所在作用域(在这里是全局作用域),因此foo() 不会导致 ReferenceError。但是 foo 此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。foo() 由于对 undefined 值进行函数调用而导致非法操作, 因此抛出 TypeError 异常。

即使是具名的函数表达式,名称标识符在赋值之前也无法在所在的作用域中使用。

foo();	// TypeError
bar();	// ReferenceError

var foo = function bar() {
    // ...
}

// 上面代码片段经过提升后,实际会被理解成以下形式
var foo;

foo();	// TypeError
bar();	// ReferenceError

foo = function() {
    var bar = ...self...
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 1.2 函数会首先提升,然后才是变量。

重复的变量声明会被忽略掉,但后面的函数声明会覆盖前面的。

一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:

foo(); // "b"
var a = true;
if (a) {
	function foo() { console.log("a"); }
}
else {
	function foo() { console.log("b"); }
}
1
2
3
4
5
6
7
8

但是需要注意这个行为并不可靠,在 JavaScript 未来的版本中有可能发生改变,因此应该尽可能避免在块内部声明函数。

# 1.3 小练习

  • 题目一
alert(a)  // ?
a();  // ?
var a = 3;

function a() {
    alert(10)
}
alert(a)  // ?
a = 6;
a(); // ?
1
2
3
4
5
6
7
8
9
10
  • 答案一
alert(a)  // function a() { alert(10) }
a();  // 10
var a = 3;

function a() {
    alert(10)
}
alert(a)  // 3
a = 6;
a(); // a is not a function
1
2
3
4
5
6
7
8
9
10
  • 题目二
var a = 1;
function fn1(){	
	alert(a);	// ?
	var a = 2;
}
fn1();
alert(a);	// ?
1
2
3
4
5
6
7
  • 答案二
var a = 1;
// 函数里面也是一个小仓库
function fn1(){	
	alert(a);	// undefined
	var a = 2;
}
fn1();
alert(a);	// 1
1
2
3
4
5
6
7
8
  • 题目三
var a = 1;
function fn1(){
	alert(a);	// ?
	a = 2;	
}
fn1();
alert(a);	// ?
1
2
3
4
5
6
7
  • 答案三
var a = 1;
function fn1(){
	alert(a);	// 1
    // 不是var,则找不到a,则顺着作用域链,从里到外找
    // 省略了var,a就变成了全局变量。
    // 只要调用了一次fn1,a变量就有了定义,就可以在函数外部的任何地方被访问。
    // 会由于相应变量不会马上有定义而导致不必要的混乱。
    // 严格模式下ReferenceError错误
	a = 2;	
}
fn1();
alert(a);	// 2
1
2
3
4
5
6
7
8
9
10
11
12
  • 题目四
var a = 1;
function fn1(a){ 
	alert(a);	// ?
	a = 2;
}
fn1();
alert(a);	// ?

// 另一种情况:
var a = 1;
function fn1(a){
	alert(a);	// ?
	a = 2;		
}
fn1(a);
alert(a);	// ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 答案四
var a = 1;
// 参数a相当于var a,则会预解析一个变量a
function fn1(a){ 
	alert(a);	// undefined
	a = 2;
}
fn1();
alert(a);	// 1

// 另一种情况:
var a = 1;
function fn1(a){
	alert(a);	// 1
	a = 2;		
}
fn1(a);
alert(a);	// 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 题目五
console.log(a);	// ?
var a = 2;
1
2
  • 答案五
console.log(a);	// undefined
var a = 2;
// 虽然在编译阶段会为a分配内存空间,但默认值是undefined,直到实际执行到使用该变量的行才会赋值。
1
2
3
  • 题目六
function b(){
    function a(){
        console.log(a);
    }
    a = 10;
    return;	
}
var a;
a  = 1;
b();
console.log(a);		// ?
1
2
3
4
5
6
7
8
9
10
11
  • 答案六
function b(){
    function a(){
        console.log(a);
    }
    // 沿着作用域链,从里向外查找
    // 先找到的是函数a,然后赋值为10
    a = 10;
    return;	
}
var a;
a  = 1;
b();
console.log(a);		// 1
1
2
3
4
5
6
7
8
9
10
11
12
13

# 二、暂时性死区

  1. let
  2. const

上述两者申明的函数是不会提前提升的。

  1. 类声明不会提前提升。

# 三、总结

“JS解析器” 先编译后执行

  • 编译器第一步:“找一些东西”(不会读取):var function 参数

    • 如果a=1 而不是 var a =1 ,那么a这个变量就不会找到。

    • 所有的变量,在正式运行代码之前,都提前赋了一个值:未定义

    • fn1 = function fn1(){ alert(2); }所有的函数,在正式运行代码之前,都是整个函数块。

  • JS 的预解析

    • 遇到重名的:只留一个 声明过的变量不会重复声明。
    • 函数声明比变量声明更置顶(函数在变量上面) 。
    • 函数声明,整体提升;变量声明,声明提升
console.log(lhj)
function lhj() {}
var lhj = 1
// f lhj() {}
1
2
3
4
console.log(lhj)
function lhj() {}
function lhj() {return 'lhj'}
// f lhj() {return 'lhj'}
1
2
3
4
console.log(lhj)	// undefined
var lhj = 1
1
2