7/8、掌握变量提升的处理机制

157 阅读14分钟

变量提升:在当前上下文中(全局/私有/块级),JS代码自上而下执行之前,浏览器会提前处理一下事情(可以理解为词法解析之前的一个环节)

  1. 会把当前上下文中所有带VAR/FUNCTION关键字的进行提前的声明和定义
    1. var a = 10
    2. 声明(declare):var a;
    3. 定义(defined):a = 10;
    4. 声明是创建变量的过程
    5. 定义是等号赋值的过程
  2. 带VAR的只会提前声明
  3. 带FUNCTION的会提前的声明定义

代码举例:

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

解析代码:

  1. 代码执行之前:全局上文中的变量提升
    1. var a;(只声明,不定义),默认值是:undefined
  2. 变量提升完成,代码执行
console.log(a); // 此时a是默认值:undefined。所以结果就是:undefined
var a = 12; // 

等号赋值分3步: 1. 创建值;2. 创建变量;3. 变量和值关联
但是变量提升阶段已声明,所以不会重复声明,只定义,a = 12
5. a 又被定义为 13

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

所以最后结果是:undefined,13

代码举例2:

func();
function func() {
    var a = 12;// 函数执行会形成私有上下文,a 是函数的私有变量。
    console.log('ok')
}

解析代码:

  1. 全局上下文中的变量提升
    1. func = 函数 (全局上下文中,函数会提前声明并提前定义)
  2. 变量提升完,执行代码
fucn();// ok
  1. 提示:因为变量提升阶段函数户提前声明定义,浏览器不会重复声明,所以执行完func()这句代码,便不再往下执行
    补充:
    1. 变量提升时 var和function 哪个先声明?自上而下,谁先出现就找谁,只不过不会再重复声明。
    2. 实际项目中 建议用函数表达式创建,比较严谨
    var func = function() {}
    
    因为这样在变量提升阶段就只会声明func,不会赋值,所以
    func() // func is not a function。 因为用函数表达式,只会声明,不会定义,
    var func = function() {}
    
    不过函数表达式还是建议用具名函数, 不要用匿名函数,不大符合规范。
    var func  = function aaa(){
        // 把原本作为值得函数表达式匿名函数'具名化',这个名字:aaa  只能在形成的私有上下文中使用,因为会把这个名字作为私有上文中的变量(值就是这个函数),不能在外边使用
        console.log('ok');
        console.log(aaa) // 当前函数
    };
    func()
    

代码举例3:

console.log(a); // ncaught ReferenceError: a is not defined
a = 13;
console.log(a);

解析代码:

  1. EC(G)变量提升
    1. 带var和function的才会变量提升,所以此案例中不存在变量提升,所以会报错。报错后,代码便不再往下执行。

代码举例4:

console.log('ok') // ok
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 12;
a = 13;
console.log(a);

解析代码:

  1. let 不存在变量提升,所以也就不会提前声明。
  2. 在新版本浏览器当中:走到第一行代码时,浏览器发现没有a,此时还会做一个操作,就是会看下后边代码有没有用let或const声明过a,如果有的话就会报错:不能在声明之前用它

知识点:

  1. 基于VAR或者FUNCTION 在全局上下文中声明的变量(全局变量)会映射到GO(全局对象window)上一份;基于映射机制,如果一个修改,另外一个也会跟着修改
var a = 12;
console.log(a); // 12  全局变量
console.log(window.a); // 12  映射到GO上得属性a
window.a = 13
console.log(a) // 13 映射机制是一个修改另外一个也会修改

练习题:

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

代码解析:

  1. EC(G): 全局上下文中的变量提升

    1. 判断体中,不论条件是否成立,都要进行变量提升(细节点:在判断体中,带FUNCTION的 在新版本浏览器中只会提前声明,不会再提前赋值了)

    2. 老版本中(新老版本以什么来划分呢?一般以IE10及IE10以内都是老版本):

      1. var a;
      2. func = 函数
    3. 新版本中:

      1. var a;
      2. func;
  2. 当然我们能现在还是按新版本来说.
    已知不管判断条件是否成立,都要进行变量提升,因为a是全局的变量,前边也提过全局变量对象VO会映射的全局对象GO中,且在浏览器端GO就是指向window的,所以此时相当于给window设置可属性a: window.a,属性func:window.func

if(!('a' in window)) { // 已知存在window.a ,所以'a' in window 条件成立,取反就是条件不成立
    var a = 1;
    function func() {}
  }
  console.log(a)
  1. 等同于以下代码
if(false) { 
    var a = 1;
    function func() {}
  }
  console.log(a) // undefined
  1. 条件不成立,所以就没办法执行判断体里边的代码,a也就没有赋值,只声明不定义(赋值)的话默认值就是:undefined
  2. 最后一行console.log(a) // undefined

和上题有类似地方的经典面试题:

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

重复下现在最新版本浏览器特点:

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

解析:

  1. 先按老版本来算(IE10及以下)
    1. EC(G)全局变量提升
      1. var a;
      2. 遇到判断体了,不管条件是否成立,都要进行变量提升
      3. 不管是var a 还是fucntion a 都是声明一个变量。所以var a 会声明一个a,function a 则不在重复声明a,而是直接定义(赋值)a
        1. var a;
        2. a = 函数
    2. 代码执行,第一行代码
    var a = 0;// 变量提升阶段 a已经声明过,此时只赋值就好:a = 10
    
    1. 代码往下执行,判断体条件成立,a 被重新赋值为1:a = 1
    2. 再往下执行遇到 function a(){},在变量提升阶段已声明+定义a,所以这一行代码不会再执行
    3. 代码往下执行,a = 21
    4. 所以输出a结果是21
    5. 全局代买输出a也是21
    6. 最后结果是21,21
  2. 按新版本来算
    1. 重复知识点:
      1. 存在块级作用域,大括号中只要出现let/const/function....就会被认为是块级作用域
      2. 不论条件是否成立,带function的只提前声明,不会提前赋值了
    2. EC(G)变量提升:
      1. var a;
      2. 不管判断体条件是否成立都要变量提升,但是不定义,所以:function a(已知浏览器不会重复定义,所以此时全局变量对象中只有 a)
    3. 代码执行
      1. 遇到 var a = 0; a赋值为0:a = 0
      2. 代码往下执行遇到判断体,且条件成立并且里边有function,所以会形成一个块级作用域
        1. 块级作用域是私有上下文,起名为:EC(BLOCK) 2 EC(BLOCK)进栈执行,把EC(G)压缩到底部
        2. 形成VO(BLOCK)(私有变量对象,也可以用AO((BLOCK),个人习惯问题)
        3. 初始化作用域链:<EC(BLOCK),EC(G)>
        4. 初始化this:没有自己的this,用的是上下文中的THIS(和箭头函数类型)
        5. 初始化ARGUMENT:无
        6. 形参赋值:无
        7. 变量提升
          1. 把大括号看作一个私有上下文,找到里边所有带var和function的 2.在私有上下文中:function要声明加定义(是的,总共要定义两次,1一次是全局的:只定义不声明,2次是块级作用域下 作为私有变量 声明+定义)
            1. a = 函数(存在AO(BLOCK)中)
        8. 代码执行
           a = 1; // EC(BLOCK)中变量都是私有的:a 是私有的, 赋值为1
          function a() {};
          a = 21;
          console.log(a)
          
          第二行代码: 变量提升阶段已处理过,不会重复操作。 因为要兼容ES3/ES56,function a 在全局下声明过,也在私有下处理过,遇到此行代码 ,私有上下文中不会再处理,但是浏览器会把当前代码之前所有对a的操作映射给全局一份 ,以此兼容ES3,但是它后边的代码和全局没有任何关系了
          第三行:a = 21 ; 此行开始,以后代码和全局都没有关系,私有变量a的值为:21
          第四行:输出私有变量a,结果是:21
        9. 私有上下文EC(BLOCK)执行完毕,出栈,继续执行全局代码
        10. 全局代码:输出a,结果是:1

练习题:

var a = 1;
var b = 23;
function asd() {
    console.log(a) //undefind
    var a = 1
    console.log(a) // 1
    console.log(b) // 23
}
if(true) {
    a = 2
}
console.log(func)
if(false) {
    function func() {}
}
asd()
console.log(a) //2 
console.log(window.a) // 2 

代码解析:
从js代码运行机制开始,复习一下

  1. 代码执行是在ECStack中执行的

  2. 全局代码执行会形成全局执行上下文EC(G),

  3. 全局上下文进栈

  4. 全局代码准备执行

  5. 全局代码执行前,变量提升,

  6. 在EC(G)中:变量提升(只发生在当前上下文中)

    1. var a;
    2. var b;
    3. asd = 函数
    4. 遇到if(true){} 判断体,不管条件是否成立,都会变量提升,判断体内只有一个变量a,a已经被声明,做过的事情不会在做了,所以此步忽略
    5. 代码再往下,遇到以下代码,不管判断条件是否成立,有var/function,都会进行变量提升注意:新版本浏览器中,function变量提升 只声明不定义
      func = undefined;
    if(false) {
        function func() {}
    }
    
    1. 全局变量提升完毕,代码正式执行
  7. 代码自上而下执行

    1. 创建值1,a与1 关联
    2. 创建值23,b与23关联
    3. 函数asd 已在变量提升阶段声明加定义
    4. if(true),条件成立,a变成2
    5. console.log(func) ; 只声明,未定义,所以此时结果是:undefined
    6. asd() 函数执行
      1. 函数执行会形成自己的私有执行上下文EC(FUN),EC(FUNC)进栈执行,将EC(G)压缩到栈的底部,等待EC(FUNC)执行完毕后,再执行EC(G)
      2. 在EC(FUNC)中会有一块空间,AO 私有变量对象,这是用来存储私有变量的
      3. 函数正式执行前还要定义作用域,变量提升....
      4. 作用域:创建函数/判断体/块级作用域 所在的上下文
      5. 作用域链(scopeChain):<EC(FUN), EC(G)>。作用域链组成规则:<自己所在的私有上下文,创建函数时的上下文>
      6. 作用域链作用:在代码私有上下文执行的时候,遇到变量,先在AO中查找此变量是否是自己私有的,如果不是,则根据作用域链找上级上下文中是否存在此变量...知道找到全局
      7. EC(FUNC)中变量提升
        1. var a;// a存在私有变量对象AO中,只定义,不声明,值默认为undefined
      8. 代码执行
        1. console.log(a) => undefined
        2. var a = 1; a 赋值为 1
        3. console.log(a) => 输出1
        4. console.log(b) => 自己的AO中并没有b,根据作用域链项EC(G)中查找,EC(G)中的 VO中存有变量b,所以输出结果为:23
        5. 代码执行完毕
      9. 代码执行完毕,EC(FUNC)并没有被占用,所以出栈,继续执行EC(G)
      10. 继续执行全局代码:console.log(a) ,a已经在 if(true) 判断体中 赋值为2,所以此时输出结果是:2
      11. 最后执行 console.log(window.a) 而且接下来是一个修改,另外一个也会跟着修改
        1. 基于var和function在全局上下文声明的变量(全局变量)会映射到GO(全局对象window)上一份,所以输出结果为2(并且其中一个值修改,另一个也会修改)

变量提升特点总结:

  1. 变量提升只发生在当前上下文中,把当前上下文中所有带var和function的要提前进行变量声明。带var的提前声明,带function的提前声明加定义
  2. 在全局上下文当中通过var/function声明过的变量或者函数,也会映射到GO(windown),给GO(windown)增加相应的属性,通过映射机制,其中一个修改,另一个也会修改
  3. 特殊情况:带条件判断的,不管条件是否成立,判断体中如果有带var/function一定会进行变量提升。在老版本浏览器(IE10及以下)中,function是声明加定义的。 在新版本浏览器中,为了向后兼容ES6,function是只提前声明,不提前定义的。
  4. 如果在当前上下文中,已经声明过变量,在变量提升阶段,不会再重复声明,但会重新赋值

练习题:
EC(G)变量提升

  1. 带var/function的

    1. fn指向一个输出1的函数。 fn => 1
    2. 遇到第二个function fn,不再声明fn,但是指向新值 =>2
    3. => 3
    4. var fn; fn已经声明过
    5. => 4
    6. => 5
    7. 变量提升结束,此时全局上下文中只有一个全局变量fn,值是输出5的函数(此时window.fn => 5)
  2. 代码执行:

    1. fn() => 5
    2. fn() => 5
    3. fn() => 5
    4. 代码走到
    var fn = function() {
        console.log(3)
    }
    

    这步时,var fn不用再处理,但是没有在变量提升阶段赋值,所以此时要处理,fn => window.fn => 3

    1. 接连三个函数调用,所以输出3,3,3
fn(); // 5
function fn() {
    console.log(1) // 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn() // 5
function fn() {
    console.log(2)// 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn() // 5
var fn = function() {
    console.log(3) // 变量提升阶段没赋值,此时赋值为输出3的函数
}
fn() // 3 
function fn() {
    console.log(4) // 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn()
function fn() {
    console.log(5)// 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn() // 5

最后结果是:5 5 5 3 3 3

练习题:

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

  1. 执行环境栈供代码执行

  2. 全局代码执行,形成EC(G),全局上下文

  3. EC(G)进栈执行

  4. 创建VO全局变量对象,存储全局变量

  5. VO会映射到GO全局对象

  6. 变量提升

    1. var foo
    2. bar = 函数(创建堆AAAFFF000,bar = 函数会映射到GO)
      1. 堆内存中储存 代码字符串和键值对(name,prototype,——proto——等)
  7. 代码执行 foo = 1(结果同时会映射到GO)

  8. bar() 函数执行

    1. 函数执行形成全新的私有上下文EC(BAR)
    2. EC(BAR)进栈执行,EC(G)被压缩到底部
    3. 创建AO私有变量对象,用来存储私有变量
    4. EC(BAR)正式执行前还要做很多事情
      1. 定于作用域<EC(BAR),EC(G)>
      2. 初始化this:window
      3. 初始化ARGUMENT
      4. 形参赋值....
      5. 变量提升
        1. 判断体条件是否成立,var都会进行变脸提升:var foo。 foo会存到AO中
        2. 代码执行

    5.代码执行:

    if(!foo){
      var foo = 10
    }
    
    1. foo 是个变量,先在自己私有变量对象AO中查找,已知,foo 已给提前声明,未定义,默认值是undfined
    2. undefined 转布尔值是false,取反就是true
    3. 条件成立,执行判断体中的代码
    4. foo = 10 赋值
    5. 此时私有变量foo的值是10
    6. 私有上下文中的代码:console.log(foo),所以输出的是四哟变量foo的值,结果是:10

练习题:

var a = 10;
(function(){
    console.log(a) // undefined
    a = 5
    console.log(window.a)
    var a = 20
    console.log(a)
})()

解析:

  1. EC(G)变量提升 var a(存到VO(G)中);
  2. 代码执行
    1. a = 10
    2. 自执行函数执行
    3. 函数作用域链:<EC(自执行),EC(G)>
    4. EC(自执行)变量提升
      1. var a(存到AO中);
    5. 函数内代码执行,console.log(a), AO中存有私有变量a,但没有定义,默认值是undefined,所以输出结果也:undefined
    6. 私有变量a = 5
    7. console.log(window.a), AO(G)中的对象会映射到GO(window),所以输出结果是:10
    8. 私有变量a = 20
    9. console.log(a) : 20

也就是等价于:

var a = 10;
(function(){
    var a;
    console.log(a) // undefined
    a = 5
    console.log(window.a)
    a = 20
    console.log(a)
})()

练习题:

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})(); // Goodbye Jack

也就是等价于:

var name = 'World!';
(function () {
    var name;
    if (typeof name === 'undefined') {
        name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

练习题:

f1();
console.log(b); // 9
console.log(c); // 9
console.log(a); a is not defined
 
function f1(){
    var a = b = c = 9;
    console.log(a);     // 9
    console.log(b);     // 9
    console.log(c);     // 9
  }
  1. EC(G)变量提升 f1 = 函数
  2. 代码执行,f1()
  3. 函数内代码变量提升
    1. var a = b = c = 9; 等价于
      var a = 9 ; b = 9 ; c = 9;
    2. 带var的提升:var a
    3. a = 9
    4. 依次输出a,b,c的结果是:9 9 9
  4. f1()执行完,执行全局的代码,依次输出 b,c,a
  5. b和c 没有带var 的情况下 看作是windown的属性,也就是全局的变量,所以输出b,c 依旧是:9 9
  6. a并没有定义,所以输出:a is not difined