創作者:張敏
轉載請聯系“極光學苑”授權!
在JavaScript的語法中,涉及到變量和函數的使用時我們初學都會記住一個規律,那就是 " 先定義,后使用 " 。
如果在沒有定義時使用某個變量或函數,執行過程會出現一個引用錯誤的報錯信息,提示變量或函數未定義。
另外,JavaScript的代碼執行時,總體上有一個從上往下執行的過程(特殊結構除外)。
但是,有時候寫程序過程中,我們會發現如果先使用一個在后面定義的變量或函數,程序并不會報錯。這其中的原因在于JavaScript的解析器的解析原理,下面我們詳細的給大家來解釋一下。
在解釋前,我們先來看一道面試題:
圖示1
很多面試的朋友乍一看到這個簡短的代碼,按照慣常思維從上往下執行代碼,粗心之下如果給出 "out" 的答案,那么面試的結果就不會樂觀了。
有網友確實因為這樣一道看似簡單的題目,失去了工作機會。
那么,真正的答案是什么呢?控制臺輸出 undefined 。
想了解其中的原理,不要錯過接下來給大家的講解。
JavaScript 代碼的執行是由瀏覽器中的 JavaScript 解析器來執行的。
JavaScript 解析器執行 JavaScript 代碼的時候,分為前后兩個過程:
預解析過程和代碼執行過程。
預解析過程:
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代碼需要兩個階段
一、什么是預解釋
在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中內存的分類
三、全局作用域下帶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 后面的代碼雖然不執行, 但是會進行預解析
*請認真填寫需求信息,我們會在24小時內與您取得聯系。