JS中的变量提升机制

2,040 阅读11分钟

在当前执行上下文中(不论是全局,还是函数执行私有的),JS代码自上而下执行之前,首先会默认把所有带VARFUNCTION关键字的进行声明或者定义。

  • VAR的只是提前声明
  • FUNCTION的会提前声明+定义

思维导图

一、先简单了解两个语法

var n = 100为例,我们之前讲过的操作步骤应该是:

    1. 创建值100存储到栈内存中(引用数据类型首先创建堆,存完数据后,把堆的地址存放到栈中)
    1. 创建一个变量 var a;
    1. 让变量和值关联在一起

这是我们之前说过的创建变量并赋值的过程;

  • 然而在这个过程中,我们所谓的创建变量的专业描述叫做=>变量声明(declare)
  • 让变量和值关联在一起的专业描述叫做 =>变量定义(defined)

上面我们所说的是我们常规的创建变量并赋值的操作。

那当我们只声明变量时,也就是var a;但是没有给变量赋值(也就是未定义),所以默认值是undefined未定义 (这也是undefined的由来)

二、理解变量提升的含义

为了更好的理解变量提升,我们还是得先从浏览器的机制说起:

1、浏览器运行机制

  • 1、为了能够让代码执行,浏览器首先会形成一个执行环境栈ECStack(Execution Context Stack)
    • 栈内存的作用:
      • 1.代码执行
      • 2.基本数据值存储在栈内存中
    • 堆内存作用:(本次我们用不到,只是提到栈内存时,自然提一下堆内存)
      • 1.存储引用数据类型的值

    这里我们不在过多讲解堆栈内存,前面已经专门讲过。

有了栈内存代码就可以自上而下的执行了,只不过刚开始,是要把全局下的代码先执行;

  • 2、开始执行全局下的代码,就会形成一个全局执行上下文EC(GLOBAL 简写 为G)(上下文就是上文下文加一起就是环境的意思);

    • 相当于在栈内存中形成了一个小空间,用来执行全局下的代码;
    • 以后我们的函数执行也会形成一个这样的小空间,用来执行函数中的代码,这个我们一会说;

    1、2 两步整体原理:我们的目的是为了执行代码,所以围绕代码执行引发的下列操作:

    • ==> 浏览器加载页面,想让代码执行,首先会形成一个栈内存(执行环境栈 ECStack);然后开始让代码准备执行;
    • ==> 最开始要执行的一定是全局下的代码,
    • ==> 此时形成一个全局代码的执行环境(全局执行上下文 EC(G))
    • ==> 形成之后,把EC(G)压缩到栈内存中去执行(进栈);每一个函数的执行也是这样操作的...
    • ==> 有些上下文在代码执行完成后,会从栈内存中移除去(出栈),但是有些情况是不能移出去的(例如:全局上下文就不能移除...);
    • ==> 在下一次有新的执行上下文进栈的时候,会把之前没有移除的都放到栈内存的底部,让最新要执行的在顶部执行
  • 3、对应代码会在自己所属的执行上下文中执行,而这个环境中有一个存放变量的地方:变量对象(VO/AO)

    • 全局下存储变量的地方叫做:全局变量对象VO(G) ->全称Variable Object (Global)
      • 所谓的变量对象,就是专门为了存储当前环境下要创建的变量的地方

在我们之前的理解,创建完一系列的环境后,就可以执行代码了,然而并没有,在代码执行之前还有一系列的操作要做;例如:词法解析 => 形参赋值 => 变量提升...等好多好多事情;词法解析和形参赋值我们后面会说,现在我们先来理解:变量提升

2、变量提升

变量提升:就是在当前上下文中,JS代码执行之前要做的事情;首先会默认把所有带VARFUNCTION关键字的进行声明或者定义。

  • VAR的只是提前声明
  • FUNCTION的会提前声明+定义

我们以下题为例

console.log(a);
console.log(func);
var a = 10;
function func(){
    console.log(b);
    var b=20;
    console.log(b);
}
func();
console.log(a);
console.log(func); 
  • 第一步:找到带VAR和先声明,找到带FUNCTION的声明+定义
    • 1、var a ;
    • 2、function func(){...};(按照函数创建的步骤,即声明又定义)
      • 1).创建堆内存,把函数体内的代码当作字符串存储进堆内存中;
      • 2).把堆内存的十六进制地址存进栈内存;
      • 3).创建变量func与十六进制地址关联;
  • 第二步:代码开始自上而下执行
    • 1、conlose.log(a):=> 此时在当前执行上下文中,已经声明过a但是并没有赋值,所以打印结果为undefined
    • 2、console.log(func):=> 此时当前上下文中已经声明了func变量,并且已经赋值,所以打印func的结果为f func(){...}
    • 3、var a = 10:=> 之前我们在变量提升阶段,已经完成了var a操作(浏览器是又懒惰性的,做完了这件事,不会在做第二遍了),所以此时只创建了一个值10,没有在创建变量a;把10与之前在变量提升阶段声明的变量a关联;
    • 4、function func(){...}:=> 上面我们说了,同一件事情浏览器不会做两便;很明显之前在变量提升阶段已经做过这件事了,所以此步就会跳过;直接进行下一步;
    • 5、func():=> 让函数执行;
      • 目的:也是执行之前存储到堆内存中的代码,既然是要执行代码;就要形成一个执行上下文;这个上下文就叫做私有上下文EC(func随便起的名字)
      • 1).浏览器处理过程:
        • 形成后也同样要压缩进入执行环境栈中执行,
        • 此时,执行环境栈中已有的全局执行上下文就会向下压缩(给函数的执行上下文腾出执行空间)
        • 私有上下文中存储变量的地方叫做:私有变量对象AO(func1随便起的名字) ->全称Active Object;
      • 2).函数执行过程:
        • 第一步:变量提升:找到带VAR和先声明,找到带FUNCTION的声明+定义
          • var b
          • 没有带function
        • 第二步:函数执行
          • console.log(b):=> 此时b以声明为定义,所以是undefined;
          • var b=20:=> 同全局一样,跳过var b,直接创建值,并与之声明的b关联;
          • console.log(b):=> 此时,b已经被赋值,所以结果为20;
          • 函数执行完成
      • 3).浏览器处理:
        • 函数执行完成,出栈,
        • 执行环境栈中空间被释放(以后再讲,这里先这样写),
        • 之前被压缩到下面的全局执行上下文又回到原来的位置;
        • 继续执行全局下的代码;
    • 6、console.log(a):=> 此时a的值为10;
    • 7、console.log(func):=> 此时func依然指向堆内存所对应的十六进制地址,所以打印结果为f func(){...};

三、避免变量提升在函数执行时的不严谨性(不是重点简单介绍)

上面我们简单的理解了变量提升的含义;

  • 那么我们思考一下这个题,是否会报错呢?
func();
function func(){
    console.log('OK');
}
+ 答案是不会报错
func();
var func = function (){
    console.log('OK');
}

+ 答案是会报错:
    +  变量提升阶段:var func; 只定义不赋值 (默认值是undefined)
    + //=> undefined() =>Uncaught TypeError: func is not a function 

对比上面两题的区别,我们得出结论:

  • 函数表达式方式创建函数,在变量提升阶段,并不会给函数赋值,这样只有代码执行的过程中,函数赋值后才可以执行(不能在赋值之前执行) =>符合严谨的执行逻辑 =>真实项目中推荐的方式
  • 为了保证语法的规范性,JS中原本创建的匿名函数,我们最好设置一个名字,但是这个名字在函数外面还是无法使用的,只能在本函数体中使用(一般也不用) 例如:
var func = function anonymous() {
	// console.log(anonymous); //=>当前这个函数
};
// console.log(anonymous); //=>Uncaught ReferenceError: anonymous is not defined
func();

//================================================

var func = function func() {
	console.log(func); //=>函数名func
};
console.log(func); //=>变量func,存储的值是函数
func();

四、变量提升在条件判断下的处理

截止目前(注意是目前),我们的上下文只有两种:

  • => 全局上下文
  • => 函数执行产生的私有上下文

1、全局上下文中带VAR

/*
 * 全局上下文中的变量提升:
 *    [VO(G) 全局变量对象]
 *    var n;  不论IF条件是否成立都进行(只要不在函数里面,带VAR的都要变量提升)
 *    无论是IF还是FOR中的VAR都进行提升;
 */
console.log(n); //=>undefined
if (1 > 1) { //=>条件不成立
	var n = 100;
}
console.log(n); //=>undefined

2、全局上下文中带FUNCTION

-1)[IE 10及以前以及谷歌等浏览器低版本状态下]:function func(){ ... } 声明+定义都处理了

-2)[最新版本的浏览器中,机制变了]:function func; 用判断或者循环等包裹起来的函数,在变量提升阶段,不论条件是否成立,只会先声明

/*
 * 全局上下文中的变量提升:
 *   [VO(G) 全局变量对象]
 *     [IE 10及以前以及谷歌等浏览器低版本状态下]
 *       function func(){ ... }  声明+定义都处理了
 * 
 *     [最新版本的浏览器中,机制变了]
 *       function func;  用判断或者循环等包裹起来的函数,在变量提升阶段,不论条件是否成立,此处只会先声明
 */
console.log(func); //=>undefined
if (1 === 1) {
	// 此时条件成立,进来的第一件事情还是先把函数定义了(迎合ES6中的块作用域) => func=function(){ .... }
	console.log(func); //=>函数
	function func() {
		console.log('OK');
	}
	console.log(func); //=>函数
}
console.log(func); //=>函数

1、在当前执行上下文中,不管条件是否成立,变量提升是有效的

2、[IE 10及以前以及谷歌等浏览器低版本状态下]:function func(){ ... } 声明+定义都处理了

3、[最新版本的浏览器中,机制变了]:function func; 用判断或者循环等包裹起来的函数,在变量提升阶段,不论条件是否成立,只会先声明 重点在写一遍强调:

五、变量提升的两道经典面试题

1、写出结果

// 浏览器有一个特征:做过的事情不会重新再做第二遍(例如:不会重复声明)
/*
 * 全局上下文中的变量提升
 *     fn = function(){ 1 }  声明+定义
 *        = function(){ 2 }
 *     var fn; 声明这一步不处理了(已经声明过了)
 *        = function(){ 4 }
 *        = function(){ 5 }
 * 结果:声明一个全局变量fn,赋的值是 function(){ 5 }
 */
fn(); //=>5
function fn(){ console.log(1); }  //=>跳过(变量提升的时候搞过了)
fn(); //=>5
function fn(){ console.log(2); }  //=>跳过
fn(); //=>5
var fn = function(){ console.log(3); } //=>var fn; 这一步跳过,但是赋值这个操作在变量提升阶段没有搞过,需要执行一次  => fn = function(){ 3 }
fn(); //=>3
function fn(){ console.log(4); } //=>跳过
fn(); //=>3
function fn(){ console.log(5); } //=>跳过
fn(); //=>3

2、分别写出在低版本浏览器和高版本浏览器下的输出结果

低版本

/*
 * 低版本浏览器(包含IE10及以内) 
 * 全局上下文中变量提升
 *    没有
 */
f=function (){return true;};//给GO中设置一个属性 f = function () {return true;}
g=function (){return false;};//给GO中设置一个属性 g = function () {return false;}
(function () {
	/* 
	 * 自执行函数执行,形成一个私有的执行上下文
	 *    [变量提升]
	 *    function g(){return true;}
	 */
	// 条件解析:
	// g() => 私有的G执行 TRUE
	// []==![] => []==false => 0==0 => TRUE
    if (g() && [] == ![]) { //=>条件成立
        f = function () {return false;} //f不是自己私有的,则向上查找,属于全局对象中的f,此处是把全局对象中的 f = function () {return false;}
        function g() {return true;} //跳过(变量提升处理过了)
    }
})();
console.log(f()); //=>FALSE
console.log(g()); //=>FALSE  这个G找全局的(函数里面的G是自己私有的)

高版本

/*
 * 高版本浏览器 
 * 全局上下文中变量提升:没有
 */
f=function (){return true;};//给GO中设置一个属性 f = function () {return true;}
g=function (){return false;};//给GO中设置一个属性 g = function () {return false;}
(function () {
	/* 
	 * 自执行函数执行,形成一个私有的执行上下文
	 *    [变量提升]
	 *    function g; 高版本浏览器中,在判断和循环中的函数,变量提升阶段只声明不定义
	 */
	// 条件解析:
	// g() => undefined() => Uncaught TypeError: g is not a function 下面操作都不在执行了
    if (g() && [] == ![]) {
        f = function () {return false;} 
        function g() {return true;}
    }
})();
console.log(f());
console.log(g());