4. 一篇文章搞定JS变量提升问题

96 阅读9分钟

1.变量提升的概念

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

// 带var的只会提前的声明
// 带function会提前的声明定义

// 声明declare: var a; 定义defined:a = 10
var a = 10;

2.变量提升的例题

  • 例1:
// 代码执行之前:全局上下文中的变量提升 var a; 默认是undefined
console.log(a); //=> undefined

var a = 12; //=> a=12 创建值12,不需要再声明a了,因为变量提升阶段完成了,完成的事情不会重新处理

a = 13; //=> 全局变量a=13

console.log(a); //=> 13
  • 例2
// 全局上下文中的变量提升
//   + func=函数 函数在这个阶段赋值都做了
func(); //=>'OK'
function func(){
    var a = 12;
    console.log("OK");
}
  • 例3
// 真实项目中建议 用函数表达式创建函数,因为这样在变量提升阶段只会声明func,不会赋值
func(); //=>Uncaught TypeError: func is not a function
var func = function(){
    console.log('OK')
}
  • 例4
// 把原本作为值得函数表达式匿名函数“具名化”(在外边访问不到)
// 当函数执行,在形成得私有上下文中,会把这个具名化得名字作为私有上下文中的变量(值就是这个函数)来进行处理
var func = function AAA(){
    console.log("OK");
    console.log(AAA); //=>当前函数
    AAA(); // 递归调用,而不用严格模式下都不支持的 arguments.callee
}
AAA(); //=>Uncaught ReferenceError: AA is not defined 函数表达式在外界是忽略名字的,访问不到
console.log(typeof(AAA)); //=>浏览器暂时性死区:typeof中放一个未声明的变量是undefined
func();
  • 例5
console.log(a); //=>uncaught ReferenceError: a is not defined 后面不再运行
a = 13;
console.log(a);
  • 例6
/**
 * EC(G):全局作用域、全局上下文
 *   词法解析、变量提升:
 *      fn —— AF0/1/2/3 此处为最后一个函数
 */
fn();//5
function fn(){ console.log(1); }
fn();//5
function fn(){ console.log(2); }
fn();//5
// 此处代码在变量提升阶段只声明了fn,可以不重复声明但是赋值操作一定是要继续进行的
var fn = function fn(){ console.log(3); }
fn();//3
function fn(){ console.log(4); }
fn();//3
function fn(){ console.log(5); }
fn();//3

3.有关词法检测和重复声明的问题

// 在浏览器开辟栈内存供代码自上而下执行之前,不仅有变量提升的操作,还有很多的其他的操作
// 词法解析/词法检测:就是检测当前即将要执行的代码是否会出现语法错误(SyntaxError)
//   + 如果出现错误,代码将不会在执行(第一行都不会执行)
//   + 使用`let /const`等重复再声明这个变量就是语法错误
console.log("OK");//=>"OK"
console.log(a);//=>Uncaught ReferenceError:Cannot access 'a' before initialization 不能在let声明之前使用变量
let a = 12;
a = 13;
console.log(a);
var a = 12;
console.log(a); //=>12 全局变量
console.log(window.a); //=>12 映射到GO上的属性a

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

4.条件判断中的变量提升

/**
 * EC(G):全局上下文中的变量提升
 *   不论条件是否成立,都要进行变量提升
 *   细节点:条件中带function的在新版本浏览器中只会提前声明,不会再提前的赋值了
 * 
 *   老版本:
 *      var a;
 *      func=函数
 *   新版本
 *      var a; 全局上下文中声明一个a也相当于 window.a
 *      func;
 */
console.log(a,func); // => undefined undefined
if(!("a" in window)){ // "a" in window 检测a是否为window的一个属性  !true => false
    var a = 1;
    function func (){}
}
console.log(a);//=>undefined

5.自执行函数

// 自执行函数:前面加的()、!、-、~、+只有一个目的,让语法符合而已
// 自执行函数本身不进行变量提升(没名字)
(function(n){})(10);
~function(n){}(10);
-function(n){}(10);
!function(n){}(10);
+function(n){}(10);

6.全局上下文、私有上下文执行顺序

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

image.png

6.1 私有栈内存中的变量处理——作用域链查找机制

/**
 * EC(G)全局执行上下文:
 *   变量提升: 
 *     a:undefined b:undefined fn:堆内存0x000,字符串
 *   代码执行
 *     a=12 b=12->13 
 *     fn(); 函数开始执行
 */
console.log(b); // undefined
var a = 12,
    b = 12;
function fn(){
    /**
     * 函数执行会形成“私有的”作用域、栈内存、执行上下文EC(FN)
     *   初始化作用域链:<EC(FN), EC(G)>
     *   初始化this:window
     *   初始化arguments
     *   形参赋值:...
     *   变量提升:a:undefined 
     *   代码执行: a=13
     */
    console.log(a, b);// undefined 12
    var a = b = 13; // 把全局改为b=13
    console.log(a, b);// 13 13 
}
fn();
console.log(a, b);// 12 13

/**
 * 私有栈内存中代码执行的时候,如果遇到一个变量
 *   1.首先看是否为自己家的,是自己的以后操作都用自己的,不是自己的去上级作用域查找...一直找到全局作用域为止
 *   2.找到拿来用,找不到可能会报错
 * => "作用域链查找机制"
 */
 
var a = 10,
    b = 20;
<=> var a = 10;
    var b = 20;
   
var a=b=10; <=> var a=10; b=10;(b不带var)

7.有关变量提升的题

  • 例1:完整解析顺序
/**
* EC(G)
*  变量提升:把当前上下文中所有带var / function 进行提前的声明或者定义
*      【全局上下文中,基于var/function声明的变量,也相当于给window设置了对应的属性】
*          var a;
*          var b;
*          var c;
*          fn = 0x000 ; [[scope]]:EC(G)
* `代码执行
*/
console.log(a,b,c); //=> undefined undefined undefined
var a = 12,
   b = 13,
   c = 14;

function fn(a){ //代码执行遇到创建函数的代码会直接的跳过:因为在遍历提升阶段已将处理过了
   /**
    * EC(FN)私有上下文
    *  作用域链:<EC(FN),EC(G)>
    *  形参赋值:a=10
    *  变量提升:---
    *  代码执行
    */
   console.log(a,b,c);//=> 10 13,14
   a = 100;// 私有a=100
   c = 200;// 全局c=200
   console.log(a,b,c); //=> 100 13 200

   //函数执行完成后:没有返回值(RETURN)、出栈释放
}
b = fn(10); // 先把函数执行,执行的返回结果赋值给全局变量b b=undefined
console.log(a,b,c);//=>12 undefined 200

image.png

  • 例2
/**
* EC(G)
*  变量提升:
*      var i;
*      A = 0x000; [[scope]]:EC(G)
*      var y;
*      B = 0x001; [[scope]]:EC(G)
*  代码执行
* 
*/
var i = 0;

function A(){
   /**
    * EC(A1) 闭包
    *  作用域链:<EC(A1),EC(G)>
    *  形参赋值:--
    *  变量提升:
    *      var i;
    *      x = 0x100; [[scope]]:EC(A1)
    */
   var i = 10;

   function x(){
       /**
        * EC(x1)
        *  作用域链:<EC(x1),EC(A1)>
        *  形参赋值:--
        *  变量提升:--
        */

       /**
        * EC(x2)
        *  作用域链:<EC(x2),EC(A1)>
        *  形参赋值:--
        *  变量提升:--
        */
       console.log(i);// 10 10
   }
   return x; 
}
var y = A(); //y= 0x100; 
y(); //0x100() => 10

function B(){
   /**
    * EC(B) 闭包
    *  作用域链:<EC(B),EC(G)>
    *  形参赋值:--
    *  变量提升:
    *      var i;
    */
   var i = 20;
   y();// 0x100()
}
B(); // 0x001()
//函数执行,他的上级作用域(上下文)是谁,和函数在哪执行是没有关系的
//“只和在哪创建有关系”:在哪创建的,他的[[scope]]就是谁,也就是它的上级上下文就是谁
  • 例3
/**
* EC(G)
*  变量提升:
*      var a;
*      var obj;
*      fn = 0x000; [[scope]]:EC(G)
*  代码执行
*/ 
var a = 1;
var obj ={ //obj = 0x001
   "name" :"tom"
}
function fn(){
   /**
    * EC(FN)
    *  作用域链:<EC(FN),EC(G)>
    *  形参赋值:--
    *  变量提升:
    *      var a2;
    */
   var a2 = a; //私有 a2 =1
   obj2 = obj; // window.obj2 = 0x001
   a2 = a; //私有 a2 =1
   obj2.name = 'jack'; //把全局0x001堆内存中的那么修改为’Jack‘
}
fn();
console.log(a);//=>1
console.log(obj);//=>{name:'jack'}
  • 例4
/**
* EC(G)
*  变量提升
*      var a;
*      fn=0x000; [[scope]]:EC(G)
*  代码执行
*/
var a = 1;
function fn(a){
   /**
    * EC(FN)
    *     作用域链:<EC(FN),EC(G)>
    *     形参赋值:a=1
    *     变量提升:
    *         var a; 这一步浏览器会忽略,因为a私有变量已经存在于AO(FN)中了
    *         a = 0x001; [[scope]]:EC(FN) 不会重复声明,但是需要重新赋值
    */
   console.log(a);//函数0x001
   var a = 2;//=>a=2
   console.log(a);//=>2
   function a(){/*直接跳过,变量提升已经搞过了*/}
   console.log(a);//=>2
}
fn(a); // fn(1)
console.log(a);//=>1
  • 例5
/**
* EC(G)
*  变量提升
*      var a;
*      fn=0x000; [[scope]]:EC(G)
*  代码执行
*/
console.log(a);//=>undefined
var a = 12;
function fn(){
   /**
    * EC(FN)
    *  作用域链:<EC(FN),EC(G)>
    *  形参赋值:--
    *  变量提升:
    *      var a;
    */
   console.log(a);//=>undefined
   var a = 13;
}
fn();
console.log(a);//=>12

============================

/**
* EC(G)
*  变量提升
*      var a;
*      fn=0x000; [[scope]]:EC(G)
*  代码执行
*/
console.log(a);//=>undefined
var a = 12;

function fn(){
   /**
    * EC(FN)
    *  作用域链:<EC(FN),EC(G)>
    *  形参赋值:--
    *  变量提升:--
    */
   console.log(a); //=>12
   a = 13;// 全局a=13
}
fn();
console.log(a); //=>13
  • 例6
/**
* EC(G)
*  变量提升:
*      fn=0x000; [[scope]]:EC(G)
*  代码执行
*/
console.log(a); //获取一个变量,首先看是否为自己私有变量,不是自己私有的,则按照作用域链查找,看是否为上级上下文的...一直到全局上下文为止!如果全局下也没有这个变量,则继续看window是否有这个属性,如果也没有这个属性,则直接报错:a is not defined ,后面代码不会执行
a = 12;
function fn(){
   console.log(a);
   a = 13;
}
fn();
console.log(a);

  • 例7
/**
* EC(G)
*  变量提升
*      var foo;
*  代码执行
*/
var foo = 'hello';
(function (foo){
   /**
    * EC(AMY)
    *  作用域链:<EC(SNY),EC(G)>
    *  形参赋值:foo = 'hello'
    *  变量提升:
    *      var foo; (无需重复声明)
    */
   console.log(foo);//=>'hello'
   // A||B:A为真返回A的值,A为假返回B的值
   // A&&B:A为真返回B的值,A为假返回A的值
   // ||和&&同时出现的时候,&&的优先级是高于||
   var foo = foo || 'world'; // foo='hello'
   console.log(foo); //=>foo='hello'
})(foo);// 自执行函数(立即执行函数)执行:传递实参 'hello'
console.log(foo);//=>foo='hello'
  • 例8
{
   function foo(){}
   foo = 1;
}
console.log(foo);

{
   let foo = 1;
   function foo(){} // Uncaught SyntaxError:Identifier 'foo' has already been declared
}

image.png

image.png

  • 例9
// 在真实项目中,千万不要把function这个操作凡在除了函数和对象的大括号中
{
   function foo(){}
   foo = 1;
   function foo(){}//把这一步之前的foo同步给全局
   foo = 2; //私有的是2
   console.log(foo); //=>2
}
console.log(foo);//=>1
  • 例10-1
var x = 1;
function func(x,y=function anonymous1(){x=2}){
   x=3;
   y();
   console.log(x);
}
func(5);
console.log(x);

image.png

  • 例10-2
var x = 1;
function func(x,y=function anonymous1(){x=2}){
    var x=3;
    y();
    console.log(x);
}
func(5);
console.log(x);

image.png

  • 例10-3
var x = 1;
function func(x,y=function anonymous1(){x=2}){
    /**
     * 私有作用域
     *  形参赋值
     *      x = 5
     *      y = function anonymous1()
     */
    /**
     * 给块级作用域同步x,y默认值
     * 变量提升
     *      x = 5
     *      y = function anonymous1()
     * 形参赋值
     *      x = 5 => 3
     *      y = function anonymous2() 0x000 EC(EC(Y),BLOCK)
     * 代码执行
     */
    var x=3;//块级x=3
    var y = function anonymous2(){x=4}//修改块级x=4 y=function anonymous2()
    y();
    console.log(x);//=>4
}
func(5);
console.log(x);//=>全局 1