JS預解析

  首先我們先來說一下js的解析順序。js引擎讀取一段js代碼,首先會進行預解析,也就是從上往下逐行讀取代碼,尋找所有的var和function(這個後面會詳細解釋)。當預解析完成後,js引擎在從第一行開始逐行運行js代碼。
  
JS預解析的定義:
   在當前作用域下,js運行之前,會把帶有var和function關鍵字的事先聲明,並在內存中安排好。然後再從上到下執行js語句。預解析只會發生在通過var定義的變量和function上。
 1.var
  通過var關鍵字定義的變量進行預解析的時候:都是聲明declare,不管它有沒有賦值,都會賦值undefined。

alert(a); //undefined
var a = 1;
alert(b); //undefined
var b = function(){
}
alert(c); //undefined
var c;

只要是通過var定義的,不管是變量,還是函數,都是先賦值undefined,如果是變量,也不管變量有沒有賦值,在預解析階段,都是會被賦值爲undefined,在真正執行的時候纔會爲它賦值。
 2.function
  function進行預解析的時候,不僅是聲明而且還定義(define)了,但是它存儲的數據的那個空間裏面存儲的是代碼是字符串,沒有任何意義。

alert(a); //彈出的是下面的function
function a(){
  alert("預解析function")
}

  我們知道,定義函數的方式有函數聲明和函數表達式兩種。解析器在向執行環境中加載數據時,對函數聲明和函數表達式並非一視同仁。解析器會率先讀取函數聲明,並使其在執行任何代碼之前可用(可以訪問);至於函數表達式,則必須等到解析器執行到它所在的代碼行,纔會真正被解釋執行。

alert(sum(10,10));
function sum(num1,num2)
{
    return num1+num2;
} 

以上代碼完全可以正確執行,因爲在代碼開始執行之前,解析器就已經通過一個名爲函數聲明提升(function declaration hoisting)的過程,讀取並將函數聲明添加到執行環境中。對代碼求值時,JavaScript引擎在第一遍會聲明函數並將它們放到源代碼樹的頂部。所以,即使聲明函數的代碼在調用它的代碼後面,JavaScript引擎也能把函數聲明提升到頂部。如果像下面的例子所示,把上面的函數聲明改爲等價的函數表達式,就會在執行的時候出現錯誤。

alert(sum(10,10));
var sum=function(num1,num2)
{
    return num1+num2;
}; 

以上代碼會在運行的時候出現錯誤,原因在於函數位於一個初始化語句中,而不是一個函數聲明。換句話講,在執行到函數所在的語句之前,變量sum中不會保存有對函數的引用。
注: 在JS解析器進行預解析的時候會遵循一個原則,變量聲明和函數聲明重名的話會優先存儲函數聲明,與先後順序無關。

提醒!!! 這裏有一個特例,函數聲明只能出現在程序或函數體內。從句法上講,它們 不能出現在Block(塊)({ … })中,例如不能出現在 if、while 或 for 語句中。

function f(){
    console.log("outside f()");
}
+function (){
    if(false){
        function f(){
            console.log("inside f()");
        }
    }
      f(); // Uncaught TypeError: f is not a function
}();

此處調用f會報錯,甚至連外層的f也取不到,這是爲什麼呢?
條件語句塊中聲明的函數,其效果等同於在此處聲明的函數表達式。
在你這裏,由於變量提升,以+開頭的匿名函數裏面,首先聲明變量f,然後到了條件語句內部纔會定義其函數,而你的條件語句是永遠失敗的,所以這裏你只會找到未初始化的變量f,而不是函數f。所以報的錯也是TypeError,一個變量怎麼能通過 f(); 這種函數的方式調用呢。
報錯代碼相當於:

function() {
    if (false) {
        var f = function() { console.log("inside f()"); };
    }
}

或:

var f;
if(false){
    f = function(){
        console.log("inside f()");
    }
}
//這兩段代碼效果是一樣的,就相當於:
if(false){
  var a=1;
}
console.log(a); //undefined 而不是a is not defined,因爲變量提升

總結:
  Block(塊) 中只能包含Statement語句, 而不能包含函數聲明這樣的源元素。另一方面,仔細看一看規則也會發現,唯一可能讓表達式出現在Block(塊)中情形,就是讓它作爲表達式語句的一部分。但是,規範明確規定了表達式語句不能以關鍵字function開頭。而這實際上就是說,函數表達式同樣也不能出現在Statement語句或Block(塊)中(因爲Block(塊)就是由Statement語句構成的)。
  函數聲明在條件語句內雖然可以用,但是沒有被標準化,也就是說不同的環境可能有不同的執行結果。所以嚴格來講,或者按照新的標準,這種寫法是不規範的,所以大家還是不要這樣寫。非要這樣用的話,就寫成函數表達式吧……

 var foo;
  if (true) {
    foo = function() {
      return 'first';
    };
  }
  foo();

最後,再說一下最開始那個function前面的+ 吧。

平時我們可能對添加括號來調用匿名函數的方式更爲習慣:

(function(){alert('iifksp')})()        // true
//或
(function(){alert('iifksp')}())        // true

雖然上述兩者括號的位置不同,不過效果完全一樣。
那麼加了括號,讓整個語句合法做的事情只有一件,就是讓一個函數聲明語句變成了一個表達式。

function a(){alert('iifksp')}        // undefined

這是一個函數聲明,如果在這麼一個聲明後直接加上括號調用,解析器自然不會理解而報錯:

function a(){alert('iifksp')}()       
// SyntaxError: unexpected_token

因爲這樣的代碼混淆了函數聲明和函數調用,以這種方式聲明的函數a,就應該以 a(); 的方式調用。

但是括號則不同,它將一個函數聲明轉化成了一個表達式,解析器不再以函數聲明的方式處理函數a,而是作爲一個函數表達式處理,也因此只有在程序執行到函數a時它才能被訪問。

所以,任何消除函數聲明和函數表達式間歧義的方法,都可以被解析器正確識別。比如:

!function(){alert('iifksp')}()        // true
var i = function(){return 10}();      // undefined
1 && function(){return true}();       // true
1, function(){alert('iifksp')}();     // undefined

賦值,邏輯,甚至是逗號,各種操作符都可以告訴解析器,這個不是函數聲明,它是個函數表達式。並且,對函數一元運算可以算的上是消除歧義最快的方式,感嘆號只是其中之一,如果不在乎返回值,這些一元運算都是有效的:

!function(){alert('iifksp')}()        // true
+function(){alert('iifksp')}()        // NaN
-function(){alert('iifksp')}()        // NaN
~function(){alert('iifksp')}()        // -1

甚至下面這些關鍵字,都能很好的工作:

void function(){alert('iifksp')}()        // undefined
new function(){alert('iifksp')}()         // Object
delete function(){alert('iifksp')}()      // true

所以,括號做的事情也是一樣的,消除歧義纔是它真正的工作,而不是把函數作爲一個整體,所以無論括號括在聲明上還是把整個函數都括在裏面,都是合法的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章