JS的变量提升机制

465 阅读5分钟

变量提升

变量提升:在当前上下文中(全局/私有/块级),JS代码自上而下执行之前,浏览器会提前处理一些事情(可以理解为词法解析的一个环节,词法解析一定发生在代码执行之前)。会把当前上下文中所有带var和function关键字的进行提前的声明或者定义。带var的只会提前声明,带function会提前的声明和定义。变量提升的意义在于,能够在创建变量代码之前使用这个变量而代码不会报错。

举例说明

example1

console.log(a);//=>undefined
var a = 12;//=>创建值12,不需要再声明a了(变量提升阶段完成了,完成的事情不会重新处理)a=12赋值
a = 13;//全局变量a=13
console.log(a);//=>13

对于以上代码,

  • 代码执行之前,全局上下文中的变量提升:var a; 默认值是undefined
  • 代码自上而下执行,第一行输出undefined
  • 第二行,创建值12,不需要再声明a了(因为变量提升阶段完成声明了,完成的事情不会重新处理),a=12赋值
  • 第三行,创建值13,全局的a=13
  • 第四行,输出13

example2

func();
function func() {
	console.log('OK');
}

对于以上代码,

  • 代码执行之前,全局上下文中的变量提升:func = 函数,函数在这个阶段赋值都做了。
  • 代码执行,输出结果是OK

需要提出的是,在真实项目中建议使用函数表达式创建函数,以为这样在变量提升阶段只会声明func,不会赋值。

//func(); //Uncaught TypeError:func is not a function
var func = function (){
	console.log('OK');
};
func();

一般规范化写法,还要把匿名函数具名化,把原来作为值的函数表达式匿名函数“具名化”(虽说是起了名字,但是这个名字不能在外面访问=>也就是不会在当前上下文中创建这个名字)。当函数执行,在私有的上下文中,会把这个具名化的名字作为私有上下文中的变量(值就是这个函数)来进行处理。

var func = function AAA(){
	console.log('OK');
    console.log(AAA);//=>输出当前函数
    //AAA();//递归调用 而不用严格模式下都不支持的arguments.callee了
};
//AAA();//Uncaught ReferenceError:AAA is not defined
func();

example3

console.log(a);
a = 13;
console.log(a);

对于以上代码,

  • EC(G)下的变量提升,由于没有带var或者带function的代码,所以不会有变量提升
  • 执行第一行代码时候就会报错:Uncaught ReferenceError: a is not defined

example4

console.log(a);
let a = 12;
a = 13;
console.log(a);

对于以上代码,

  • EC(G)下的变量提升,由于没有带var或者带function的代码,所以不会有变量提升。注意:ES6中的let/const不会进行变量提升
  • 执行第一行代码时候就会报错:Uncaught ReferenceError: Cannot access 'a' before initialization 意思是,不能在let声明之前使用变量

**基于var或者function在全局上下文中声明的变量(全局变量)会映射到GO(全局对象window)上一份,作为它的属性;而且接下来是一个修改,另外一个也会跟着修改。**严格模式也是如此。

example5

//"use strict";
var a = 12;
console.log(a);//=>全局变量
console.log(window.a);//=>12 映射到GO上的属性a

window.a = 13;
console.log(a);//=>13  映射机制是一个修改另外一个也会修改

example6

if (!("a" in window)) {
	var a = 1;
    function func() {}
}
console.log(a);

对于以上代码,

  • EC(G)全局上下文中的变量提升:无论条件是否成立,都要进行变量提升(细节点:条件中带function的在新版本浏览器中只会提前声明,不会再提前的赋值了)
    • [老版本(ie10及以下)]var a; func= 函数;
    • [新版本]var a; func; 全局上下文中声明一个a也相当于window.a;全局上下文中声明一个func也相当于window.func
  • "a" in window 是检测a是否为window的一个属性
  • (!("a" in window))=>!ture=>false,条件不成立
  • 输出结果undefined

example7

fn();
function fn() { console.log(1); }
fn();
function fn() { console.log(2); }
fn();
var fn = function() { console.log(3); }
fn();
function fn() { console.log(4); }
fn();
function fn() { console.log(5); }
fn();

对于以上代码,

  • EC(G)变量提升
    • 声明加定义一个fn[=>1];重新定义fn[=>2];var fn已经声明过了,不再重新声明(var也不定义);再重新定义fn[=>4],;再重新定义fn[=>5]
    • 变量提升结束时,全局上下文中有一个全局变量fn,值是输出5的函数(此时window.fn=>5)
  • 代码执行
    • 第一个fn();输出5
    • 第二个fn();输出5
    • 第三个fn();输出5
    • var fn不用再处理了,但是赋值在变量提升阶段没有处理,此处需要处理,fn = window.fn =>3
    • 第四个fn();输出3
    • 第五个fn();输出3
    • 第六个fn();输出3

example8

var foo = 1;
function bar() {
	if (!foo) {
    	var foo = 10;
    }
    console.log(foo);
}
bar();

对于以上代码,具体的处理过程见下图:

example9-怪诞的面试题

var a = 0;
if (true) {
	a = 1;
    function a() {};
    a = 21;
    console.log(a);
}
console.log(a);

此题目在新老浏览器中的结果是不一样的。现在最新版本的浏览器:

  • 需要向前兼容ES3/5规范:
    • 判断体和函数体等不存在块级上下文,上下文只有全局和私有
    • 不论条件是否成立,带function的都要声明且定义
  • 还需要向后兼容ES6规范
    • 存在块级作用域,大括号中出现let/const/function都会被认为是块级作用域
    • 不论条件是否成立,带function的只提前声明,不会提前赋值了

此题的具体解析,见下图: