首先我們先來說一下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
所以,括號做的事情也是一樣的,消除歧義纔是它真正的工作,而不是把函數作爲一個整體,所以無論括號括在聲明上還是把整個函數都括在裏面,都是合法的。