js的预编译

159 阅读9分钟

segmentfault.com/a/119000001…

blog.csdn.net/qq_44624386…

www.cnblogs.com/chengxs/p/1…

预编译阶段:声明语句,如果是函数声明会直接赋值;预编译阶段结束会逐行执行,此时赋值语句才会执行

涉及:单线程、语法分析、预编译(GO、AO)、执行上下文、栈、作用域、解释执行、变量提升、函数提升、作用域链

  • js是单线程语言:在浏览器中一个页面永远只有一个线程在执行js脚本代码,不会发生解析阻塞

js运行三部曲:语法分析、预编译、解释执行

  • 语法分析:引擎检查你的代码有没有什么低级的语法错误

  • 预编译:在内存中开辟一些空间,存放一些变量与函数——会导致变量提升

  • 解释执行:执行代码

  • 浏览器先按照js的顺序加载script标签分隔的代码块,js代码块加载完毕之后,立刻进入到上面的三个阶段,然后再按照顺序找下一个代码块,再继续执行三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的,并且都在同一个全局作用域中。

过程:

  1. 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
  2. 第一个脚本文件加载;
  3. 脚本加载完毕后,分析语法是否合法;
  4. 开始预编译查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,作为GO属性,值赋予函数体;

语法分析

  • js的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用:分析该js脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误(syntaxError) ,停止改js代码的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入到预编译阶段。

预编译

  • GO(global object)对象(window对象)(脚本代码块script执行前)

    1. 生成GO对象 GO{}(global object) 这个GO就是window

    2. 将全局的变量声明(的名)储存一GO对象中,value为undefinde

    3. 将全局的函数声明的函数名作为go对象中的key,函数整体内容为value储存到GO对象中

    • 任何变量,如果变量未经声明就赋值,这些变量就为全局对象所有。一切声明的全局变量和未经声明的变量,全归window所有。

      var a = 123;
      window.a = 123;
      function test(){
      	// 这里的b是未经声明的变量,所以是归window所有的。
      	var a = b = 110;
      }
      
    • 例子

      var a
      function fun() {}
      function abc() {}
      console.log('第1次', a)      // function a
      function a() {}
      console.log('第2次', a)      // function a
      var a = 100 
      console.log('第3次', a)      // 100
      
      1. 会生成一个对象(GO),这个对象封装的就是作用域,称为GO(global object)。当全部挂载完成之后,然后代码再去逐行执行

      2. 分析变量声明(var)——变量作为GO对象的属性名,值为undefined

      3. 分析函数声明(function)——函数名作为GO对象的属性名,值为函数体(如果遇到同名,直接覆盖)

        GO={
             a:undefined,
             fun:function fun(){},
             abc:function abc(){}                                                                                                                       
        }
        
      4. 当走到某一行的时候;a产生了一次赋值;此时GO对象改变了

      5. 逐行执行(看着GO对象里面的执行)

  • AO(action object)对象

    • 是函数执行前的一瞬间,生成一个AO对象(在函数执行前的一瞬间会生成自己的AO,如果函数执行2次,生成了两次AO,这两次的AO是没有任何关联)

      1. 执行前的一瞬间,会生成一个AO(action object)对象
      2. 分析参数,形参作为AO对象的属性名,实参作为AO对象的属性值
      3. 分析var变量声明,变量名作为AO对象的属性名,值为undefined,如果遇到同名的,不去做任何改变
      4. 分析函数声明,函数名作为AO对象的属性名,值为函数体,如果遇到同名的,直接覆盖(会将作为参数传进来的变量值覆盖)
    • 例子

      var num = 100
      function fun(num) {
      	console.log(num) // 5
      }
      fun(5)
      // 1.调用前的一瞬间,生成fun.AO对象
      AO = {}
      // 2.分析参数
      AO = {
      	num : 5
      }
      // 3.分析变量声明  没有略过
      // 4.分析函数声明  没有略过
      AO = {
      	num : 5
      }
      
      console.log(person) // undefined
      console.log(personFun) // function personFun
      var person = "saucxs";
      console.log(person) // saucxs
      function personFun() {
          console.log(person)
          var person = "songEagle";
          console.log(person)
      }
      personFun() // undefined songEagle
      console.log(person) // saucxs
      

变量提升、函数提升

  1. 函数声明整体提升-(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)

  2. 变量声明提升-(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)

    function dd() {
    	return 4
    }
    var dd
    console.log(dd) //function dd()
    
    function test () {
        console.log(a);  //undefined 变量提升
        var a = 123
    }
    function test () {
        console.log(a);  // 报错
    }
    
    a = 1;
    var a;
    console.log(a); //1
    

函数调用栈

  • 每进入到一个不同的运行环境都会创建 一个相应的执行上下文(execution context),那么在一段js程序中一般都会创建多个执行上下文,js引擎会以栈的数据结构对这些执行进行处理,形成函数调用栈(call stack),栈底永远是全局执行上下文(global execution context),栈顶则永远时当前的执行上下文。

    function bar() {
        var B_context = "bar saucxs";
        function foo() {
            var f_context = "foo saucxs";
        }
        foo()
    }
    bar()
    
    1. 首先进入到全局环境,创建全局执行上下文(global Execution Context ),推入到stack中;
    2. 调用bar函数,进入bar函数运行环境,创建bar函数执行上下文(bar Execution Context),推入stack栈中;
    3. 在bar函数内部调用foo函数,则再进入到foo函数运行环境中,创建foo函数执行上下文(foo Execution Context),如上图,由于foo函数内部没有再调用其他函数,那么则开始出栈;
    4. foo函数执行完毕之后,栈顶foo函数执行上下文(foo Execution Context)首先出栈;
    5. bar函数执行完毕,bar函数执行上下文(bar Execution Context)出栈;
    6. 全局上下文(global Execution Cntext)在浏览器或者该标签关闭的时候出栈。

    说明:不同的运行环境执行都会进入到代码预编译和执行两个阶段,语法分析则在代码块加载完毕时统一检查语法。

创建执行上下文

  1. 创建变量对象(variable object)(创建GO、AO对象)

  2. 创建作用域链(scope chain)

    • 作用域链由当前执行环境的变量对象(未进入到执行阶段前)与上层环境的一系列活动对象组成,保证了当前执行还款对符合访问权限的变量和函数有序访问。

    1. 作用域链的第一项永远是当前作用域(当前上下文的变量对象或者活动对象);
    2. 最后一项永远是全局作用域(全局上下文的活动对象);
    3. 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或者函数,找到则会停止找,找不到则一直查找全局作用域,再找不到就会排除错误。
    var num = 30;
    function test() {
        var a = 10;
        function innerTest() {
            var b = 20;
            return a + b
        }
        innerTest()
    }
    test()
    
    innerTestEC = {
    
        //变量对象
        VO: {b: undefined}, 
    
        //作用域链
        scopeChain: [VO(innerTest), AO(test), AO(global)],  
        
        //this指向
        this: window
    }
    
  3. 确定this的指向

作用域

  1. 全局作用域(GO)

    1. 全局作用域在页面打开时被创建,页面关闭时被销毁
    2. 编写在script标签中的变量和函数,作用域为全局,在页面的任意位置都可以访问到
    3. 在全局作用域中又全局对象window,代表一个浏览器窗口,由浏览器创建,可以直接调用
    4. 全局作用域中声明的变量和函数会作为window对象的属性和方法保存
  2. 函数作用域(AO)

    1. 调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁
    2. 每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
    3. 在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量
    4. 在函数作用域中访问变量、函数时,会先在自身作用域中寻找,若没有找到,则会在函数的上一级作用域中寻找,一直到全局作用域

作用域链

  • 会被保存在一个隐式的属性中去[scope]这个属性是我们用户访问不到的,但是的的确确是存在的,让js引擎来访问的,里面存储的就是作用域链

  • 作用域是AO和GO,作用域链是AO和GO的集合 例子

    ```
    function a() {
    	function b() {
    		var bb = 123
    		a = 0	
    	}
    	var a = 234
    	b()
    }
    a()
    ```
    
  • a定义的时候作用域链首端指向全局GO

  • a执行和b定义同时进行,产生a的作用域AO,作用域链首端指向AO,作用域链的第二端指向全局作用域GO

  • b执行,顺序分别指向b的AO(this指向window),a的AO,全局的GO

闭包

  • 例子

    function a() {
    	var a = 123
    	function b() {
    		var bb =234
    		console.log(aa) // 123
    	}
    }
    var res = a()  // a执行完毕要将作用域销毁
    res()
    
  • a执行完毕要将作用域销毁,但是b的作用域链没有销毁

  • b函数被保存在外部,但仍然可以指向a的作用域AO,这是产生闭包的原因

  • 单例模式实现闭包

    var createLogin = function() {
    	var div = document.createElement('div')
    	div.innerHTML = '我是登录的弹窗'
    	div.style.display = 'none'
    	document.body.appendChild(div)
    	return div
    }
    var getSingle = function(fn) {
    	var result
    	return function() {
    		return result || (result = fn.apply(this, arguments)) // 第二次result就有值了
    	}
    }
    var create = getSingle(createLogin)
    document.getElementById('loginBtn').onclick = function() {
    	var loginLay = create()
    	loginLay.style.display = 'block'
    }