原生js:预编译

99 阅读7分钟

一、js的特点:

js是一门动态语言脚本语言解释型语言弱类型语言

二、什么是预编译:

js引擎会在解释JavaScript代码前先对其进行编译。

三、预编译的时机:

js运行时做三件事:

  1. 语法分析:对代码进行通篇检查,排除语法错误
  2. 预编译
  3. 解释执行:解释一行,执行一行

预编译分为全局预编译局部预编译

  • 全局预编译在页面加载完后,检查没有语法错误,执行第一行代码前执行,产生GO对象
  • 局部预编译在函数执行前的一瞬间执行,产生AO对象,函数执行完销毁AO。也就是说,函数执行两次,产出了两次AO,但这两个AO并无关联

四、预编译的步骤:

全局预编译的步骤:

  1. 生成GO(global object)对象,这个GO就是window,是全局执行期上下文,默认有this、window、document三个属性
  2. 查找变量的声明,作为GO对象的属性名,值为undefined
  3. 查找函数的声明,作为GO对象的方法名,值为function

局部预编译的步骤:

  1. 在函数执行前,为当前函数创建AO(activation object)对象,它是函数执行期上下文,在调用结束销毁,默认有this、arguments两个属性
  2. 查找形参和变量作为AO对象的属性名,值为undefined
  3. 使用实参的值改变形参的值
  4. 查找函数的声明作为AO对象的方法名,值为function

要注意:

①if判断中的var也会在预编译阶段进行变量提升,if语句到执行阶段才执行

    if (x) {
      var b = 1 
    }

②函数的AO是在函数执行的前一刻创建的,函数不执行就不会创建AO;但如果函数中使用了暗示全局变量,当函数执行的前一刻,该变量会被保存到GO中

      function test() {
        x = 100
      }
      test()
      console.log(x) // 100

五、由预编译的过程推导优先级的问题

1、JavaScript中的变量名和函数名重名时,函数的优先级更高

      a()
      function a() {
        console.log(b) // ƒ b() {}
        var b
        function b() {}
      }
      var a
      console.log(a) // ƒ a() { ... }

为什么打印a和b时都是函数?

因为不管是全局预编译还是局部预编译,他们的步骤中最后一步是查找函数的声明作为GO或AO的方法名,此时如果在前面有同名的属性或方法,则会覆盖之前的属性或方法,最终的效果就是函数的提升会比变量的提升优先级高。

2、在函数内,优先级排序:局部函数 > 实参 > 形参/局部变量

1、形参和局部变量重名,以实参为准。因为实参赋值的动作发生在查找形参和变量之后

      function a(i) {
        console.log(i) // 100
        var i = 1
      }
      a(100)

2、形参和局部函数重名,以局部函数为准。因为实参赋值的动作发生在查找函数声明之前

      function b(i) {
        console.log(i) // ƒ i() {}
        function i() {}
      }
      b(100)

六、预编译时GO和AO的演变

示例1:

      var a
      function fn() {}
      function a() {}
      console.log(a)
      var a = 100
      console.log(a)
      /*
        全局预编译的过程:
          1、生成GO对象
            GO: {}
          2、查找变量的声明,作为GO对象的属性名,值为undefined
            GO: {
              a: undefined
            }
          3、查找函数的声明,作为AO对象的方法名,值为function
            GO: {
              a: f a(){}, // 注意看,在预编译阶段a由undefined变为function了
              fn: f fn(){}
            }
          
        至此,预编译结束,开始执行代码,当执行到 a = 100 时,GO对象为:
          GO: {
            a: 100,
            fn: f fn(){}
          }
        所以,第一次打印a的位置,a为 f a(){},第二次打印a时,a为100
      */

示例2:

      function fn(a) {
        console.log(a)
        var a = 123
        console.log(a)
        function a() {}
        console.log(a)
        var b = function bb() {}
        console.log(b)
        function c() {}
        var c = a
        console.log(c)
      }
      fn(1)
      /*
        局部预编译的过程:
          1、生成AO对象
            AO: {}
          2、查找形参和变量作为AO对象的属性名,值为undefined
            AO: {
              a: undefined,
              b: undefined,
              c: undefined
            }
          3、使用实参的值改变形参的值
            AO: {
              a: 1, // 在此阶段,a的值由undefined变为1
              b: undefined,
              c: undefined
            }
          4、查找函数的声明作为AO对象的方法名,值为function
            AO: {
              a: f a(){}, // 在此阶段,a的值由1变为function
              b: undefined,
              c: f c(){} // 在此阶段,c的值由undefined变为function
            }

        至此,预编译结束,开始执行代码,第一次打印a,a为f a(){}。当执行到 a = 123 时,AO对象为:
          AO: {
            a: 123,
            b: undefined,
            c: f c(){}
          }
        第二次打印a时,a为123
        声明函数a的这句代码在预编译阶段已执行,直接跳过,第三次打印a,还是123
        执行到 b = function bb() {} 时,AO对象为:
          AO: {
            a: 123,
            b: f bb() {},
            c: f c(){}
          }
        所以打印b时,b为 f bb(){}
        声明函数c的这句代码也是在预编译阶段执行过了,直接跳过,执行 c = a,所以打印c的值为123
      */

八、imply global暗示全局变量

什么叫暗示全局变量?

如果变量未经声明直接赋值,那么该变量为全局对象所有

      function fn() {
        var a = b = 1 // 等价于 var a = 1; b = 1
      }
      fn()
      console.log(window.a)
      console.log(window.b)

由此可知,var声明的变量,和暗示全局变量,都是在操作window,在window对象上添加了某个属性或方法。那加不加var的区别是啥?

①在全局作用域下,通过var声明的变量,是为window对象添加了一个不可配置(不可删除)的属性;而不加var的变量,是为window对象添加了一个可配置(可删除)的属性

      var logo = 'volvo'
      console.log(logo) // volvo
      console.log(window.logo) // volvo
      console.log('logo' in window) // volvo

      delete window.logo // 这句代码无效
      console.log(logo) // volvo
      logo = 'volvo'
      console.log(logo) // volvo
      console.log(window.logo) // volvo
      console.log('logo' in window) // volvo

      delete window.logo // 这句代码无效
      console.log(logo) // Uncaught ReferenceError: logo is not defined

②在局部作用域下,通过var定义的变量是该函数的局部变量,并且会进行变量提升;没有通过var定义的变量,在进行读取和设置时都是在操作全局变量,如果全局作用域下没有该变量,提前读取时会报错:Uncaught ReferenceError: logo is not defined

      var logo = 'volvo'
      function fn() {
        console.log(logo) // undefined
        var logo = 'bmw' // 通过var定义的变量会进行变量提升
      }
      fn()
      var logo = 'volvo'
      function fn() {
        console.log(logo) // volvo 在当前作用域中找不到logo,就会向上查找,找到全局logo为volvo
        logo = 'bmw' // 这是对全局变量重新赋值
      }
      fn()
      console.log(logo) // bmw

③严格模式下,对未使用var声明的变量进行赋值会报错

      'use strict'
      logo = 'volvo' // Uncaught ReferenceError: logo is not defined

九、函数声明和函数表达式

函数声明:

      function fn() {}

函数表达式:

      var fn = function () {}
区别函数名是否必须调用时机如何实现自执行
函数声明必须函数提升(全局预编译已经将函数保存在GO中),可以在声明前调用function fn() { console.log('fn') }() 报语法错误,函数的声明和调用必须分开;可以使用()将函数声明包裹起来变成表达式,或者在function前加上+/-/!,然后可以在后面加上(),这就是自执行函数
函数表达式可选变量提升,但是此时fn是undefined,必须要在函数表达式执行完才能调用const fn = function () { console.log('fn') }() 直接在表达式后面加上()便可以自执行,要注意只有表达式才能被执行符号()调用

建议:

由于js中存在函数提升,在遇到if语句、for循环、try/catch/finally语句时最好不要使用函数声明,推荐任何情况下都使用函数表达式