变量提升(块作用域下的变量提升)

109 阅读6分钟

虽然现在大家写代码多数用ES6的方式声明变量,如let/const。当然也就不存在变量提升问题了,但是还是把变量提升的机制总结一下。因为工作中,而且还是大多数的工作中,都需要维护之前的代码。当然如果有一天,进步的很快,都换成新的,那么这可能就是一篇无用的文章了。废话不多说,上菜喽!

定义

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

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

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

// 因为变量提升,所以可以在fn代码之前,调用fn 
fn()
function fn(){} 

// 真实项目建议大家,定义function用 表达式的方式定义。执行在下面在执行。(这样的顺序看起来更舒服)
var func = function(){} 
func()

变量提升-块作用域下-新版本浏览器的特殊点

通过两段代码,分析运行机制,说明特殊点。

第一段代码:

{
    function foo(){}
    foo = 1
}
console.log(foo)

// 以上是一个代码块,在代码块,循环体,判断体中,如果存在let/const/function,则会产生块级私有上下文。

代码执行之前,变量提升:当前上下文中(EC(G)),出现在“非函数和对象”的大括号中的function,只声明不定义。代码执行:如果大括号中出现了function/let/const等关键字,则会形成一个全新的块级私有上下文

全局上下文EC(G)

  • 代码执行之前:

    变量提升:(特点是只声明不定义) 声明foo

  • 代码执行:

    形成块级上下文 EC(Block)

    • 代码执行之前:

      作用域链:<EC(Block),EC(G)>

      没有this和形参赋值以及arguments

      变量提升:也是把当前上下文中所有带var/function声明或者定义。以上代码 -> 声明+定义 foo

    • 代码执行 function foo(){} (当前上下文中,这一步已经处理过了,所以不再重新执行;但是因为这一步操作和全局上下文也有关系,所以:把当前这一行代码之前对foo的操作,都“映射/同步”给全局一份,但是“之后”对foo的操作都认为是改自己私有的,和全局没有关系了! )那么全局上下文中之前只声明过,这一步赋值完,同步到全局,之后操作与全局没有关系。

第二段代码

{
    function foo(){}
    foo = 1
    function foo(){}
}
console.log(foo)   // 1

全局上下文EC(G)

  • 代码执行之前:

    VO(G): foo=> undefined

    变量提升:function foo;只声明:因为出现在大括号中了

  • 代码执行:

    形成块级上下文 EC(Block)

    • 代码执行之前:

      VO(BLOCK): foo => 0X000-> 0X001

      作用域链:<EC(Block),EC(G)>

      没有this和形参赋值以及arguments

      变量提升:

      function foo(){}    函数堆 0X000  [[scope]]:EC(BLOCK)
      
      function foo(){}    不再声明,但是会创建   函数堆 0X001  [[scope]]:EC(BLOCK)
      
    • 代码执行

      function foo(){}  忽略,但是会在此映射到全局上下文
      
      foo = 1  私有的foo改成1
      
      function foo(){}  忽略,但是会把他之前的操作映射到全局上下文中的foo  将全局的改成最新的foo
      

出现这种机制的原因:

  • 向后兼容ES3,ES5的语法规范

  • 向前兼容ES6的新语法规范(块级上下文)

项目中,千万不要把function这个操作放在除了函数和对象的大括号中。

变量提升---形参加默认值的变态机制

只要符合以下条件:

  • 形参赋值有默认值,不管执行不执行,只要有默认值。
  • 函数体中声明过变量(let/const/var), function声明的变量得和形参中的一个保持一致,才会生效。即function的名字是 fn 那么形参也得有个fn。 就会触发一个全新的“变态机制”,函数执行不仅形成了一个私有的上下文,而且会把函数体当作一个块级私有上下文,并且块级上下文的作用域链的上级上下文就是上一个私有上下文 如果块级私有上下文中声明的变量,也出现在函数的形参变量中,则默认把私有上下文中的形参变量值,赋值给块级上下文中一份。[ 这个变量不能基于let/const声明, 因为let/const 不能重复定义变量 ]

执行方式: 私有上下文中形参赋值成功后,接下来的操作,都是在块级上下文中处理的,(他是把函数的大括号当作块级上下文) 如果块级上下文中的某个私有变量,和当前私有上下文中的形参变量的名字一样,还会把形参变量的值,默认给块级上下文....

项目中,尽可能少用形参默认值。

注意点

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

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

eg:

// EC(G):全局上下文的变量提升 变量提升的同时,也会映射。都在代码执行之前。
// 不论条件是否成立,都要进行变量提升(细节点:条件中带function在新版本浏览器中只会提前声明,不会提前赋值了)
// IE10(包含)以下都是老版本  新版本 ie10以上 和 其他的浏览器 

if(!("a" in window)){
    var a = 1
}
console.log(a) //undefined

复习知识点

  • 哪种情况下会产生块级上下文

    块级作用域(块级私有上下文): 除了 对象/函数(let obj={} function fn(){})的大括号之外的其他大括号,有例如:判断体,循环体,代码块,如果存在let/const/function,则会产生块级私有上下文。

    注: let/const/function会产生块级私有上下文,而var不会。

  • ||和&&用法

    A||B : A 为真 返回 A的值,B不再判断; A 为假 返回 B的值

    A&&B : A 为真 返回 B的值; A 为假 返回 A的值,B不再判断

    两者同时出现:&& 优先级高于 ||