整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript 面試雷區之預解析

          JavaScript 面試雷區之預解析

          創作者:張敏

          轉載請聯系“極光學苑”授權!

          前言

          在JavaScript的語法中,涉及到變量和函數的使用時我們初學都會記住一個規律,那就是 " 先定義,后使用 " 。

          如果在沒有定義時使用某個變量或函數,執行過程會出現一個引用錯誤的報錯信息,提示變量或函數未定義。

          另外,JavaScript的代碼執行時,總體上有一個從上往下執行的過程(特殊結構除外)。

          但是,有時候寫程序過程中,我們會發現如果先使用一個在后面定義的變量或函數,程序并不會報錯。這其中的原因在于JavaScript的解析器的解析原理,下面我們詳細的給大家來解釋一下。

          思考

          在解釋前,我們先來看一道面試題:

          圖示1

          很多面試的朋友乍一看到這個簡短的代碼,按照慣常思維從上往下執行代碼,粗心之下如果給出 "out" 的答案,那么面試的結果就不會樂觀了。

          有網友確實因為這樣一道看似簡單的題目,失去了工作機會。

          那么,真正的答案是什么呢?控制臺輸出 undefined 。

          想了解其中的原理,不要錯過接下來給大家的講解。

          JavaScript預解析

          JavaScript 代碼的執行是由瀏覽器中的 JavaScript 解析器來執行的。

          JavaScript 解析器執行 JavaScript 代碼的時候,分為前后兩個過程:

          預解析過程和代碼執行過程。

          預解析過程:

          1. 把變量的聲明提升到當前作用域的最前面,只會提升聲明,不會提升賦值。
          2. 把函數的聲明提升到當前作用域的最前面,只會提升聲明,不會提升調用。
          3. 先提升 var,在提升 function 。

          JavaScript 的執行過程:

          在預解析之后,根據新的代碼順序,從上往下按照既定規律執行 js 代碼。

          很多問題的關鍵點提煉出來,原理就是預解析過程中出現的變量聲明提升和函數聲明提升。

          變量聲明提升

          在預解析過程中,所有定義的變量,都會將聲明的過程提升到所在的全局作用域或者函數作用域最上面,在將來的代碼執行過程中,按照先后順序會先執行被提升的聲明變量過程。

          ? 提升過程中,只提升聲明過程,不提升變量賦值,變量賦值的過程會保留在原代碼位置。

          那么被提升到頂部的變量,相當于定義后未被賦值,未賦值的變量內默認存儲 undefined值。

          ? 因此,在 js 中會出現一種現象,在前面調用后定義的變量,不會報錯,只會使用 undefined值。

          圖示2

          上面這段程序的輸出結果就是 undefined 。

          如果我們用代碼來模擬 JavaScript 解析器的工作,相當于:

          圖示3

          講到這里,回想我們思考中的面試題,可以得到我們想到的解釋了。我們來將面試題的代碼解析過程也用代碼來模擬一下:

          圖示4

          這時我們再來分析,函數內部調用a變量時,根據作用域鏈的查找順序,會優先在本層函數作用域內查找是否有定義的a變量,結果找到了被提升后的a的定義,因此不會再往外查找函數外部的a。

          同時,a變量提升后,內部因為沒有賦值,存儲的為默認值 undefined,所以輸出答案就是undefined。

          對于我們的程序來說,使用變量是為了用它內部存儲的數據,而不是被提升后使 undefined,因此推薦朋友們在定義變量時,最好 直接把變量定義過程書寫在代碼最前面

          函數聲明提升

          同變量提升類似的,在預解析過程中,所有定義的函數,都會將聲明的過程提升到所在的全局作用域或者函數作用域最上面,在將來的代碼執行過程中,按照先后順序會先執行被提升的函數聲明過程。

          ?在預解析之后的代碼執行過程中,函數定義過程已經在最開始就會執行,一旦函數定義成功,那么函數名內就已經存儲了指向函數對象的地址,后續通過函數名就可以直接調用函數。

          ?因此,在 js 中會出現一種現象,在前面調用后定義的函數,不會報錯,而且能正常執行函數內部的代碼。

          圖示5

          執行結果:控制臺輸出1。

          如果我們用代碼來模擬JavaScript解析器的工作,相當于:

          圖示16

          另外,有一種特殊的函數定義方式,函數表達式:

          它進行的是變量聲明提升,而不是函數聲明提升。提升后變量內部存的是一個 undefined。在前面進行函數方法調用,數據類型會提示錯誤。

          圖示7

          結果:

          圖示8

          對于我們的程序來說,函數聲明提升可以用于調整代碼的順序,將大段的定義過程放到代碼最后,但是不影響代碼執行效果。

          因此,建議在定義函數時,最好使用 function 關鍵字定義方式,這樣函數聲明提升可以永遠生效。

          提升順序

          預解析過程中,先提升 var 變量聲明,再提升 function 函數聲明。

          ?假設出現變量名和函數名相同,那么后提升的函數名標識符會覆蓋先提升的變量名,那么在后續代碼中出現調用標識符時,內部是函數的地址,而不是 undefined。

          圖示9

          結果:

          圖示10

          如果調用標識符的過程在源代碼函數和變量定義后面,相當于函數名覆蓋了一次變量名,結果在執行到變量賦值時,又被新值覆蓋了函數的地址,那么在后面再次調用標識符,用的就是變量存的新值。

          圖示11

          結果:

          圖示12

          如果我們用代碼來模擬 JavaScript 解析器的工作,相當于:

          圖示13

          當然,以上這些特殊案例的寫法大部分會出現在面試題中,來對大家進行一個誤導,同時檢測大家對 JavaScript 基礎原理的掌握程度。在實際自行編寫代碼過程中,還是建議 不要書寫相同的標識符給變量名或函數名,避免出現覆蓋。、

          總結

          1. 變量聲明提升:只提升定義,賦值過程保留在原位置。

          2. 函數聲明提升:整個定義過程都被提升。

          3. 建議利用函數聲明提升,規避變量聲明提升。

          最后,JavaScript 中很多基本原理經常被用作前端的面試題,通過這篇文章,希望大家對預解析過程有了一個清晰的了解,再次遇到相關面試題可以輕松解決。

          想要得到更多的前端面試攻略,請關注公眾號:極光訓練營。

          掃碼添加:極光訓練營

          JavaScript是一門解釋型的語言 , 想要運行JavaScript代碼需要兩個階段

          • 編譯階段: 編譯階段就是我們常說的JavaScript預解釋(預處理)階段,在這個階段JavaScript解釋器將完成把JavaScript腳本代碼轉換到字節碼
          • 執行階段: 在編譯階段JavaScript解釋器借助執行環境把字節碼生成機械碼,并從上到下按順序執行
          • 本文就重點介紹預解釋,框架圖如下:


          一、什么是預解釋

          在js中,帶var 和function關鍵字的需要預解釋:

          那什么是預解釋?就是在js代碼執行之前,先申明好帶有var 關鍵字和帶有function關鍵字的變量,在內存里先安排好。

          預解釋:JavaScript代碼執行之前,瀏覽器首先會默認的把所有帶var和function的進行提前的聲明或者定義

          1.理解聲明和定義

          聲明(declare):如var num;=>告訴瀏覽器在全局作用域中有一個num的變量了;如果一個變量只是聲明了但是沒有賦值,默認的值是undefined

          定義(defined):如num=12;=>給我們的變量進行賦值。

          2.對于帶var和function關鍵字的在預解釋的時候操作不一樣的

          var=>在預解釋的時候只是提前的聲明

          function=>在預解釋的時候提前的聲明+定義都完成了

          3.預解釋只發生在當前的作用域下。

          例如:開始只對window下的進行預解釋,只有函數執行的時候才會對函數中的進行預解釋

          二、作用域鏈

          1.如何區分私有變量和全局變量?

          1)在全局作用域下聲明(預解釋的時候)的變量是全局變量

          2)只有函數執行會產生私有的作用域,比如for(){}、if(){}和switch(){}都不會產生私有作用域

          3)在"私有作用域中聲明的變量(var 聲明)"和"函數的形參"都是私有的變量。在私有作用域中,代碼執行的時保遇到了一個變量,首先我們需要確定它是否為私有的變量,如果是私有的變量,那么和外面的沒有在何的關系;如果不是私有的,則往當前作用域的上級作用域進行查找,如果上級作用域也沒有則繼續查找,一直找到window為止,這就是作用域鏈。

          我們舉個例子來區別私有變量和全局變量:

          //=>變量提升:var a;var b;var c;test=AAAFFF111;
          var a=10,b=11,c=12;
          function test(a){
          //=>私有作用域:a=10 var b;
          a=1;//=>私有變量a=1
          var b=2;//=>私有變量b=2
          c=3;//=>全局變量c=3
          }
          test(10);
          console.log(a);//10
          console.log(b);//11
          console.log(c);//3
          

          判斷是否是私有變量一個標準就是是否是在函數中var聲明的變量和函數的形參都是私有的變量。本道題目在test函數中a是形參和var b定義的變量b都是私有變量。

          2.函數傳參

          這是因為當函數執行的時候,首先會形成一個新的私有的作用域,然后按照如下的步驟執行:

          1)如果有形參,先給形參賦值

          2)進行私有作用域中的預解釋

          3)私有作用域中的代碼從上到下執行

          我們來看一道例題

          var total=0;
          function fn(num1,num2){
          console.log(total);//->undefined 外面修改不了私有的
          var total=num1 +num2;
          console.log(total);//->300
          }
          fn(100,200);
          console.log(total);//->0 私有的也修改不了外面的
          

          3.JS中內存的分類

          • 棧內存:用來提供一個供JS代碼執行的環境,即作用域(全局作用域/私有的作用域)
          • 堆內存:用來存儲引用數據類型的值。對象存儲的是屬性名和屬性值,函數存儲的是代碼字符串。

          三、全局作用域下帶var和不帶var的區別

          我們先來看以下兩個例子:

          //例題1
          console.log(num);//->undefined
          var num=12;
          //例題2
          console.log(num2);//->Uncaught ReferenceError:num2 is not defined 
          num2=12;//不能預解釋
          

          當你看到var num=12時,可能會認為只是個聲明。但JavaScript實際上會將其看成兩條聲明語句:var num;和 num=12;第一個定義聲明是在預解釋階段進行的。第二個賦值聲明會被留在原地等待執行階段。num2=12 相當于給window增加了一個叫做num2的屬性名,屬性值是12;而var num=12 首先它相當于給全局作用域增加了一個全局變量num,它也相當于給window增加了一個屬性名num2,屬性值是12。兩者最大區別:帶var的可以進行預解釋,所以在賦值的前面執行不會報錯;不帶var的是不能進行預解釋的,在前面執行會報錯;

          接下來我們舉例說明:

          //例題1
          var total=0;
          function fn(){
          console.log(total);//undefined
          var total=100;
          }
          fn();
          console.log(total);//0
          //例題2
          var total=0;
          function fn(){
          console.log(total);//0
          total=100;
          }
          fn();
          console.log(total);//100
          

          例題1中帶var變量在私有作用域中可以預解釋,所以第一個console打出來的值為undefined。私有作用域中出現的一個變量不是私有的,則往上級作用域進行查找,上級沒有則繼續向上查找,一直找到window為止,例題2中不帶var變量不是私有的,所以往上級找

          四、預解釋五大毫無節操的表現

          1.預解釋的時候不管你的條件是否成立,都要把帶var的進行提前的聲明。

          請看下面這道例題:

          if(!("num" in window)){
          var num=12;//這句話會被提到大括號之外的全局作用域:var num;->window.num; 
          }
          console.log(num);//undefined
          

          2.預解釋的時候只預解釋”=”左邊的,右邊的值,不參與預解釋

          請看下面這道例題:

          fn();//報錯
          var fn=function (){ //window下的預解釋:var fn;
          console.log("ok");
          };
          

          3.自執行函數:定義和執行一起完成了。

          自執行函數定義的那個function在全局作用域下不進行預解釋,當代碼執行到這個位置的時候定義和執行一起完成了。常見有以下幾種形式:

          (function(num){})(10);
          ~function(num){}(10);
          +function(num){}(10);
          -function(num){}(10);
          !function(num){}(10);
          

          4.函數體中return下面的代碼雖然不再執行了,但是需要進行預解釋;return后面跟著的都是我們返回的值,所以不進行預解釋;

          function fn(){
          //預解釋:var num;
          console.log(num);//->undefined
          return function(){};
          var num=100;
          }
          

          5.函數聲明和變量聲明都會被提升。但是一個值得注意的細節(這個細節可以出現在有多個“重復”聲明的代碼中)是函數會首先被提升,然后才是變量。在預解釋的時候,如果名字已經聲明過了,不需要從新的聲明,但是需要重新的賦值;

          我們先來看下兩個簡單的例子:

          //例題1
           function a() {}
           var a
           console.log(typeof a)//'function'
          //例題2
           var c=1
           function c(c) {
           console.log(c)
           var c=3
           }
           c(2)//Uncaught TypeError: c is not a function
          

          當遇到存在函數聲明和變量聲明都會被提升的情況,函數聲明優先級比較高,最后變量聲明會被函數聲明所覆蓋,但是可以重新賦值,所以上個例子可以等價為

          function c(c) {
          console.log(c)
          var c=3
          }
          c=1
          c(2)
          

          接下來我們看下兩道比較復雜的題目:

          //例題3
          fn();
          function fn(){console.log(1);};
          fn();
          var fn=10;
          fn();
          function fn(){console.log(2);};
          fn();
          

          1.一開始預解釋,函數聲明和賦值一起來,fn 就是function fn(){console.log(1);};遇到var fn=10;不會重新再聲明,但是遇到function fn(){console.log(2);}就會從重新賦值,所以一開始fn()的值就是2

          2.再執行fn();值不變還是2

          3.fn重新賦值為10,所以運行fn()時報錯,接下去的語句就沒再執行。

          //例題4
          alert(a);
          a();
          var a=3;
          function a(){
          alert(10)
          }
          alert(a);
          a=6;
          a()
          

          1.函數聲明優先于變量聲明,預解釋時候,函數聲明和賦值一起來,a就是function a(){alert(10)},后面遇到var a=3,也無需再重復聲明,所以先彈出function a(){alert(10)}

          2.a(),執行函數,然后彈出10

          3.接著執行了var a=3; 所以alert(a)就是顯示3

          4.由于a不是一個函數了,所以往下在執行到a()的時候, 報錯。

          對前端的技術,架構技術感興趣的同學關注我的頭條號,并在后臺私信發送關鍵字:“前端”即可獲取免費的架構師學習資料

          知識體系已整理好,歡迎免費領取。還有面試視頻分享可以免費獲取。關注我,可以獲得沒有的架構經驗哦!!

          預解析 其實就是聊聊 js 代碼的編譯和執行
          ●js 是一個解釋型語言,就是在代碼執行之前,先對代碼進行通讀和解釋,然后在執行代碼
          ●也就是說,我們的 js 代碼在運行的時候,會經歷兩個環節 解釋代碼 和 執行代碼
          ●JavaScript引擎在對JavaScript代碼進行解釋執行之前,會對JavaScript代碼進行預解析,在預解析階段,會將以關鍵字var和function開頭的語句塊提前進行處理
          ●處理過程:當變量和函數的聲明處在作用域比較靠后的位置的時候,變量和函數的聲明會被提升到作用域的開頭。

          解釋代碼和執行代碼
          ●因為是在所有代碼執行之前進行解釋,所以叫做 預解析(預解釋)
          ●需要解釋的內容有兩個
          ○var 關鍵字
          ■在內存中先聲明有一個變量名
          ■會把 var 關鍵字聲明的變量進行提前說明, 但是不進行賦值
          ○聲明式函數
          ■在內存中先聲明有一個變量名是函數名,并且這個名字代表的內容是一個函數
          ■也就是會把函數名進行提前聲明, 并且賦值為一個函數

          解析var關鍵字

          // 1. 解析 var 關鍵字
          console.log(num)
          var num=100
          console.log(num)

          代碼分析:

          預解析
          var num
          告訴瀏覽器, 我定義了一個叫做 num 的變量, 但是沒有賦值
          代碼執行
          第 1 行代碼, 在控制臺打印 num 變量的值
          因為預解析的時候, 已經聲明過 num 變量, 只是沒有賦值
          num 變量是存在的
          打印出來的是 undefined
          第 2 行代碼, num=100
          給已經定義好的 num 變量賦值為 100 這個數據
          第 3 行代碼, 在控制臺打印 num 變量的值
          因為第 2 行代碼的執行, num 已經被賦值為 100 了
          此時打印出來的內容是 100


          解析賦值式函數
          ●賦值式函數會按照 var 關鍵字的規則進行預解析

          fn()
          var fn=function() { console.log('fn 函數') }
          fn()

          ○代碼分析:
          預解析
          var fn
          告訴瀏覽器我定義了一個叫做 fn 的變量, 但是沒有賦值
          代碼執行
          第 1 行代碼, fn()
          拿到 fn 變量存儲的值當做一個函數來調用一下
          因為 fn 只是聲明了變量, 并沒有賦值, 所以 fn 是一個 undefined
          我們做的事情是, 把 undefined 當做一個函數來調用
          報錯: fn is not a function

          解析聲明式函數


          //解析聲明式函數
          fn()
          function fn() { console.log('fn 函數') }
          fn()

          ○代碼分析 :

          預解析
          function fn() { console.log('fn 函數') }
          告訴瀏覽器, 我定義了一個 fn 變量, 并且 fn 變量保存的內容是一個函數
          代碼執行
          第 1 行代碼, fn()
          拿到 fn 變量存儲的值, 當做一個函數來調用
          因為預解析階段 fn 存儲的就是一個函數
          調用沒有問題
          第 3 行代碼, fn()
          拿到 fn 變量存儲的值, 當做一個函數來調用
          因為預解析階段 fn 存儲的就是一個函數
          調用沒有問題

          預解析優先級


          fn()
          console.log(num)
          function fn() {
              console.log('我是 fn 函數')
          }
          var num=100

          ●經過預解析之后可以變形為


          function fn() {
              console.log('我是 fn 函數')
          }
          var num
          fn()
          console.log(num)
          num=100



          預解析中重名問題
          1.當你使用 var 定義變量 和 聲明式函數 重名的時候, 以 函數為主
          2.只限于在預解析階段, 以函數為準

          案例1

          num()
          var num=100
          num()
          function num() { console.log('我是 num 函數') }
          num()

          ○代碼分析 :

          預解析
          var num
          告訴瀏覽器我定義了一個叫做 num 的變量, 但是并沒有賦值
          function num() { console.log('我是 num 函數') }
          告訴瀏覽器我定義了一個叫做 num 的變量, 并且賦值為一個函數
          預解析結束階段, num 變量存在, 并且是一個函數
          執行代碼
          第 1 行代碼, num()
          拿到 num 的值當做一個函數來調用
          因為預解析階段, num 就是一個函數
          所以正常調用
          第 2 行代碼, num=100
          給 num 變量賦值為 100
          因為 num 本身保存的是一個函數, 現在賦值為 100
          就把 函數 覆蓋了, 一個變量只能保存一個值
          從此以后, num 就是 100 了
          第 3 行代碼, num()
          拿到 num 的值當做一個函數來調用
          因為第 2 行的代碼執行, 已經把 num 賦值為 100
          此時就是把 數字 100 當做一個函數來調用
          報錯: num is not a function

          案例2


          num()
          function num() { console.log('我是 num 函數') }
          num()
          var num=100
          num()

          ○代碼分析:

          預解析
          function num() { console.log('我是 num 函數') }
          告訴瀏覽器, 我定義了一個叫做 num 的變量, 并且賦值為一個函數
          var num
          告訴瀏覽器, 我定義了一個叫做 num 的變量, 但是沒有賦值
          預解析結束的時候, num 變量存在, 并且是一個函數
          代碼執行
          第 1 行代碼, num()
          把 num 存儲的值拿來當做一個函數調用
          因為預解析階段, 確定了 num 就是一個函數
          調用沒有問題
          第 3 行代碼, num()
          把 num 存儲的值拿來當做一個函數調用
          因為預解析階段, 確定了 num 就是一個函數
          調用沒有問題
          第 4 行代碼, num=100
          把 num 賦值為 100
          本身保存的函數就被覆蓋了
          從此以后, num 就是 100 了
          第 5 行代碼, num()
          把 num 存儲的值拿來當做一個函數調用
          因為第 4 行代碼的執行, 導致 num 是一個 數字 100
          把 數字 100 當做函數調用
          報錯: num is not a function

          預解析中特殊情況
          ●在代碼中, 不管 if 條件是否為 true, if 語句代碼里面的內容依舊會進行預解析


          //預解析的特殊情況
          // 1. if語句
          console.log(num) // undefined
          if (true) {
              // 第一件事: var num
              // 第二件事: num=100
              var num=100
          }
          console.log(num)

          函數體內, return 后面的代碼雖然不執行, 但是會進行預解析


          主站蜘蛛池模板: 国产在线第一区二区三区| 东京热无码一区二区三区av| 国产av熟女一区二区三区| 无码人妻一区二区三区免费n鬼沢| 日韩精品一区二区午夜成人版| 国产拳头交一区二区| 国产日韩一区二区三区| 日韩国产精品无码一区二区三区| 美女免费视频一区二区| 奇米精品一区二区三区在线观看| 亚洲一区二区三区夜色| 色一情一乱一伦一区二区三区| 中文字幕无码不卡一区二区三区| 日本一区午夜艳熟免费| 在线精品自拍亚洲第一区| 成人久久精品一区二区三区| 中文字幕一区二区三区精华液| 国产精品视频第一区二区三区| 日韩爆乳一区二区无码| 国产一区玩具在线观看| 国产人妖视频一区二区破除| 国产一区二区三区日韩精品| AV无码精品一区二区三区| 日韩一区二区在线观看| 日本激情一区二区三区| 亚洲av无码一区二区乱子伦as | 精品一区二区三区AV天堂| 久久国产免费一区二区三区| 99偷拍视频精品一区二区| 亚洲精品精华液一区二区 | 亚洲日韩国产欧美一区二区三区 | 日韩人妻无码一区二区三区久久99 | 51视频国产精品一区二区| 亚洲第一区在线观看| 3d动漫精品啪啪一区二区免费| 亚洲一区二区三区四区视频| 人妻体内射精一区二区| 精品国产一区二区三区久久久狼| 国产在线无码视频一区| 亚洲国产精品一区二区九九| 国产剧情国产精品一区|