JavaScript中的预解析

ThelmaLamb 8年前
   <p>在ES6之前,变量使用 var 声明,会存在变量的预解析(函数也有预解析),我相信很多同学在刚开始学JavaScript的时候被预解析搞得团团转,虽然在ES6的时候引入 let 和 const ,但是现阶段ES6并没有完全普及,而且很多比较老的代码都还是按照ES5的标准甚至是ES3的标准来书写的。</p>    <h2>一、变量和函数在内存中的展示</h2>    <p>JavaScript中的变量类型和其他语言一样,有基本数据类型和引用数据类型。基本数据类型包括: undefined 、 null 、 boolean 、 String 、 Number ;引用数据类型主要是对象(包括{}、[]、/^$/、Date、Function等)。</p>    <pre>  <code class="language-javascript">var num = 24;  var obj = {name:'iceman' , age:24};  function func() {      console.log('hello world');  }</code></pre>    <ul>     <li>当浏览器加载html页面的时候,首先会提供一个供全局JavaScript代码执行的环境,称之为全局作用域。</li>     <li>基本数据类型按照值来操作,引用数据类型按照地址来操作。</li>    </ul>    <p>根据以上原则,以上的代码在内存中的模型为:</p>    <p><img src="https://simg.open-open.com/show/1d2935c00492975c68ac5cc8673ea326.png"></p>    <p style="text-align:center">内存模型.png</p>    <p>基本类型是直接存储在栈内存中,而对象是存储在堆内存中,变量只是持有该对象的地址。所以obj持有一个对象的地址oxff44,函数func持有一个地址oxff66。</p>    <p>在以上的代码的基础上再执行:</p>    <pre>  <code class="language-javascript">console.log(func);  console.log(func());</code></pre>    <p>第一行输出的是整个函数的定义部分(函数本身):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/02a0f7c1b948a05a2be5e05f3269f080.png"></p>    <p style="text-align:center">第一行代码输出结果.png</p>    <p>上面已经说明了,func存储的是一个地址,该地址指向一块堆内存,该堆内存就保留了函数的定义。</p>    <p>第二行输出的是func函数的返回结果:</p>    <p><img src="https://simg.open-open.com/show/1d343e8057d99ad5652f990b39b4239e.png"></p>    <p>第二行代码输出结果.png</p>    <p>由于func函数没有返回值,所以输出undefined。</p>    <p>注意:函数的返回结果,return后面写的是什么,返回值就是什么,如果没有return,默认返回值是undefined。</p>    <h2>二、预解析</h2>    <p>有了以上的内存模型的理解之后,就能更好的了解预解析的机制了。所谓的预解析就是: 在当前作用域中,JavaScript代码执行之前,浏览器首先会默认的把所有带var和function声明的变量进行提前的声明或者定义。</p>    <p>2.1. 声明和定义</p>    <pre>  <code class="language-javascript">var num = 24;</code></pre>    <p>这行简单的代码其实是两个步骤:声明和定义。</p>    <ul>     <li><strong>声明</strong> :var num; 告诉浏览器在全局作用域中有一个num变量了,如果一个变量只是声明了,但是没有赋值,默认值是undefined。</li>     <li><strong>定义</strong> :num = 12; 定义就是给变量进行赋值。</li>    </ul>    <p>2.2. var声明的变量和function声明的函数在预解析的区别</p>    <p>var声明的变量和function声明的函数在预解析的时候有区别,var声明的变量在预解析的时候只是提前的声明,function声明的函数在预解析的时候会提前声明 <strong>并且会同时定义</strong> 。也就是说var声明的变量和function声明的函数的区别是在声明的同时有没同时进行定义。</p>    <p>2.3. 预解析只发生在当前的作用域下</p>    <p>程序最开始的时候,只对window下的变量和函数进行预解析,只有函数执行的时候才会对函数中的变量很函数进行预解析。</p>    <pre>  <code class="language-javascript">console.log(num);  var num = 24;  console.log(num);    func(100 , 200);   function func(num1 , num2) {      var total = num1 + num2;      console.log(total);  }</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/de65544bbaceb4f0fa0a0c1109fdc4b3.png"></p>    <p style="text-align:center">输出结果.png</p>    <p>第一次输出num的时候,由于预解析的原因,只声明了还没有定义,所以会输出undefined;第二次输出num的时候,已经定义了,所以输出24。</p>    <p>由于函数的声明和定义是同时进行的,所以func()虽然是在func函数定义声明处之前调用的,但是依然可以正常的调用,会正常输出300。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5c283e7b34af59c5f25394b1cbdd7c0d.png"></p>    <p style="text-align:center">内存模型.png</p>    <h2>三、 作用域链</h2>    <p>先理解以下三个概念:</p>    <ul>     <li>函数里面的作用域成为私有作用域,window所在的作用域称为全局作用域;</li>     <li>在全局作用域下声明的变量是全局变量;</li>     <li>在“私有作用域中声明的变量”和“函数的形参”都是私有变量;</li>    </ul>    <p>在私有作用域中,代码执行的时候,遇到了一个变量,首先需要确定它是否为私有变量,如果是私有变量,那么和外面的任何东西都没有关系,如果不是私有的,则往当前作用域的上级作用域进行查找,如果上级作用域也没有则继续查找,一直查找到window为止,这就是 <strong>作用域链</strong> 。</p>    <p>当函数执行的时候,首先会形成一个新的私有作用域,然后按照如下的步骤执行:</p>    <ol>     <li>如果有形参,先给形参赋值;</li>     <li>进行私有作用域中的预解析;</li>     <li>私有作用域中的代码从上到下执行</li>    </ol>    <p>函数形成一个新的私有的作用域,保护了里面的私有变量不受外界的干扰(外面修改不了私有的,私有的也修改不了外面的),这也就是 <strong>闭包</strong> 的概念。</p>    <pre>  <code class="language-javascript">console.log(total);   var total = 0;  function func(num1, num2) {      console.log(total);       var total = num1 + num2;      console.log(total);  }  func(100 , 200);  console.log(total);</code></pre>    <p>以上代码执行的时候,第一次输出total的时候会输出undefined(因为预解析),当执行func(100,200)的时候,会执行函数体里的内容,此时func函数会形成一个新的私有作用域,按照之前描述的步骤:</p>    <ul>     <li>先给形参num1、num2赋值,分别为100、200;</li>     <li>func中的代码进行预解析;</li>     <li>执行func中的代码</li>    </ul>    <p>因为在func函数内进行了预解析,所以func函数里面的total变量会被预解析,在函数内第一次输出total的时候,会输出undefined,接着为total赋值了,第二次输出total的时候就输出300。 因为函数体内有var声明的变量total,函数体内的输出total并不是全局作用域中的total。</p>    <p>最后一次输出total的时候,输出0,这里输出的是全局作用域中的total。</p>    <pre>  <code class="language-javascript">console.log(total);   var total = 0;  function func(num1, num2) {      console.log(total);       total = num1 + num2;      console.log(total);  }  func(100 , 200);  console.log(total);</code></pre>    <p>将代码作小小的变形之后,func函数体内的total并没有使用 var 声明,所以total不是私有的,会到全局作用域中寻找total,也就说说这里出现的所有total其实都是全局作用域下的。</p>    <h2>四、 全局作用域下带var和不带var的区别</h2>    <p>在全局作用域中声明变量带var可以进行预解析,所以在赋值的前面执行不会报错;声明变量的时候不带var的时候,不能进行预解析,所以在赋值的前面执行会报错。</p>    <pre>  <code class="language-javascript">console.log(num1);  var num1 = 12;    console.log(num2);  num2 = 12;</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/540d60191148c4c662ca7e8253159a7a.png"></p>    <p style="text-align:center">输出结果.png</p>    <ul>     <li><strong>num2 = 12;</strong> 相当于给window增加了一个num2的属性名,属性值是12;</li>     <li><strong>var num1 = 12;</strong> 相当于给全局作用域增加了一个全局变量num1,但是不仅如此,它也相当于给window增加了一个属性名num,属性值是12;</li>    </ul>    <p>问题:在私有作用域中出现一个变量,不是私有的,则往上级作用域进行查找,上级没有则继续向上查找,一直找到window为止,如果window也没有呢?</p>    <ul>     <li>获取值:console.log(total); --> 报错 Uncaught ReferenceError: total is not defined</li>     <li>设置值:total= 100; --> 相当于给window增加了一个属性名total,属性值是100</li>    </ul>    <pre>  <code class="language-javascript">function fn() {      // console.log(total); // Uncaught ReferenceError: total is not defined      total = 100;  }  fn();  console.log(total);</code></pre>    <p>注意:JS中,如果在不进行任何特殊处理的情况下,上面的代码报错,下面的代码都不再执行了</p>    <h2>五、 预解析中的一些变态机制</h2>    <p>5.1 不管条件是否成立,都要把带var的进行提前的声明</p>    <pre>  <code class="language-javascript">if (!('num' in window)) {       var num = 12;  }  console.log(num); // undefined</code></pre>    <p>JavaScript进行预解析的时候,会忽略所有if条件,因为在ES6之前并没有块级作用域的概念。本例中会先将num预解析,而预解析会将该变量添加到window中,作为window的一个属性。那么 'num' in window 就返回true,取反之后为false,这时代码执行不会进入if块里面,num也就没有被赋值,最后console.log(num)输出为undefined。</p>    <p>5.2 只预解析“=”左边的,右边的是指,不参与预解析</p>    <pre>  <code class="language-javascript">fn(); // -> undefined();  // Uncaught TypeError: fn is not a function  var fn = function () {      console.log('ok');  }    fn(); -> 'ok'  function fn() {      console.log('ok');  }  fn(); -> 'ok'</code></pre>    <p>建议:声明变量的时候尽量使用var fn = ...的方式。</p>    <p>5.3 自执行函数:定义和执行一起完成</p>    <pre>  <code class="language-javascript">(function (num) {    console.log(num);  })(100);</code></pre>    <p>自治性函数定义的那个function在全局作用域下不进行预解析,当代码执行到这个位置的时候,定义和执行一起完成了。</p>    <p>补充:其他定义自执行函数的方式</p>    <pre>  <code class="language-javascript">~ function (num) {}(100)    + function (num) {}(100)    - function (num) {}(100)    ! function (num) {}(100)</code></pre>    <p>5.4 return下的代码依然会进行预解析</p>    <pre>  <code class="language-javascript">function fn() {                                   console.log(num); // -> undefined      return function () {                     };                                     var num = 100;                     }                                    fn();</code></pre>    <p>函数体中return下面的代码,虽然不再执行了,但是需要进行预解析,return中的代码,都是我们的返回值,所以不进行预解析。</p>    <p>5.5 名字已经声明过了,不需要重新的声明,但是需要重新的赋值</p>    <pre>  <code class="language-javascript">var fn = 13;                                         function fn() {                                          console.log('ok');                                 }                                                    fn(); // Uncaught TypeError: fn is not a function</code></pre>    <p>经典题目</p>    <pre>  <code class="language-javascript">fn(); // -> 2                                               function fn() {console.log(1);}                             fn(); // -> 2                                               var fn = 10; // -> fn = 10                                  fn(); // -> 10()  Uncaught TypeError: fn is not a function                            function fn() {console.log(2);}                             fn();</code></pre>    <p> </p>    <p>来自:http://www.jianshu.com/p/c3276ff58c93</p>    <p> </p>