深入理解JavaScript中的預(yù)解析
前言
JavaScript是解釋型語言是毋庸置疑的,但它是不是僅在運行時自上往下一句一句地解析的呢?
事實上或某種現(xiàn)象證明并不是這樣的,通過《JavaScript權(quán)威指南》及網(wǎng)上相關(guān)資料了解到,JavaScript有“預(yù)解析”行為。理解這一特性是很重要的,不然在實際開發(fā)中你可能會遇到很多無從解析的問題,甚至導(dǎo)致程序bug的存在。為了解析這一現(xiàn)象,也作為自己的一次學(xué)習(xí)總結(jié),本文逐步引導(dǎo)你來認識JavaScript“預(yù)解析”,如果我的見解有誤,還望指正。
在ES6之前,變量使用var聲明,會存在變量的預(yù)解析(函數(shù)也有預(yù)解析),我相信很多同學(xué)在剛開始學(xué)JavaScript的時候被預(yù)解析搞得團團轉(zhuǎn),雖然在ES6的時候引入let和const,但是現(xiàn)階段ES6并沒有完全普及,而且很多比較老的代碼都還是按照ES5的標準甚至是ES3的標準來書寫的。
一、變量和函數(shù)在內(nèi)存中的展示
JavaScript中的變量類型和其他語言一樣,有基本數(shù)據(jù)類型和引用數(shù)據(jù)類型?;緮?shù)據(jù)類型包括:undefined、null、boolean、String、Number;引用數(shù)據(jù)類型主要是對象(包括{}、[]、/^$/、Date、Function等)。
var num = 24; var obj = {name:'iceman' , age:24}; function func() { console.log('hello world'); }
當(dāng)瀏覽器加載html頁面的時候,首先會提供一個供全局JavaScript代碼執(zhí)行的環(huán)境,稱之為全局作用域。
基本數(shù)據(jù)類型按照值來操作,引用數(shù)據(jù)類型按照地址來操作。
根據(jù)以上原則,以上的代碼在內(nèi)存中的模型為:
內(nèi)存模型.png
基本類型是直接存儲在棧內(nèi)存中,而對象是存儲在堆內(nèi)存中,變量只是持有該對象的地址。所以obj持有一個對象的地址oxff44,函數(shù)func持有一個地址oxff66。
在以上的代碼的基礎(chǔ)上再執(zhí)行:
console.log(func); console.log(func());
第一行輸出的是整個函數(shù)的定義部分(函數(shù)本身):
第一行代碼輸出結(jié)果.png
上面已經(jīng)說明了,func存儲的是一個地址,該地址指向一塊堆內(nèi)存,該堆內(nèi)存就保留了函數(shù)的定義。
第二行輸出的是func函數(shù)的返回結(jié)果:
第二行代碼輸出結(jié)果.png
由于func函數(shù)沒有返回值,所以輸出undefined。
注意:函數(shù)的返回結(jié)果,return后面寫的是什么,返回值就是什么,如果沒有return,默認返回值是undefined。
二、預(yù)解析
有了以上的內(nèi)存模型的理解之后,就能更好的了解預(yù)解析的機制了。所謂的預(yù)解析就是:在當(dāng)前作用域中,JavaScript代碼執(zhí)行之前,瀏覽器首先會默認的把所有帶var和function聲明的變量進行提前的聲明或者定義。
2.1. 聲明和定義
var num = 24;
這行簡單的代碼其實是兩個步驟:聲明和定義。
- 聲明:
var num;
告訴瀏覽器在全局作用域中有一個num變量了,如果一個變量只是聲明了,但是沒有賦值,默認值是undefined。 - 定義:
num = 12;
定義就是給變量進行賦值。
2.2. var聲明的變量和function聲明的函數(shù)在預(yù)解析的區(qū)別
var聲明的變量和function聲明的函數(shù)在預(yù)解析的時候有區(qū)別,var聲明的變量在預(yù)解析的時候只是提前的聲明,function聲明的函數(shù)在預(yù)解析的時候會提前聲明并且會同時定義。也就是說var聲明的變量和function聲明的函數(shù)的區(qū)別是在聲明的同時有沒同時進行定義。
2.3. 預(yù)解析只發(fā)生在當(dāng)前的作用域下
程序最開始的時候,只對window下的變量和函數(shù)進行預(yù)解析,只有函數(shù)執(zhí)行的時候才會對函數(shù)中的變量很函數(shù)進行預(yù)解析。
console.log(num); var num = 24; console.log(num); func(100 , 200); function func(num1 , num2) { var total = num1 + num2; console.log(total); }
輸出結(jié)果.png
第一次輸出num的時候,由于預(yù)解析的原因,只聲明了還沒有定義,所以會輸出undefined;第二次輸出num的時候,已經(jīng)定義了,所以輸出24。
由于函數(shù)的聲明和定義是同時進行的,所以func()
雖然是在func函數(shù)定義聲明處之前調(diào)用的,但是依然可以正常的調(diào)用,會正常輸出300。
內(nèi)存模型.png
三、 作用域鏈
先理解以下三個概念:
- 函數(shù)里面的作用域成為私有作用域,window所在的作用域稱為全局作用域;
- 在全局作用域下聲明的變量是全局變量;
- 在“私有作用域中聲明的變量”和“函數(shù)的形參”都是私有變量;
在私有作用域中,代碼執(zhí)行的時候,遇到了一個變量,首先需要確定它是否為私有變量,如果是私有變量,那么和外面的任何東西都沒有關(guān)系,如果不是私有的,則往當(dāng)前作用域的上級作用域進行查找,如果上級作用域也沒有則繼續(xù)查找,一直查找到window為止,這就是作用域鏈。
當(dāng)函數(shù)執(zhí)行的時候,首先會形成一個新的私有作用域,然后按照如下的步驟執(zhí)行:
- 如果有形參,先給形參賦值;
- 進行私有作用域中的預(yù)解析;
- 私有作用域中的代碼從上到下執(zhí)行
函數(shù)形成一個新的私有的作用域,保護了里面的私有變量不受外界的干擾(外面修改不了私有的,私有的也修改不了外面的),這也就是閉包的概念。
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);
以上代碼執(zhí)行的時候,第一次輸出total的時候會輸出undefined(因為預(yù)解析),當(dāng)執(zhí)行func(100,200)的時候,會執(zhí)行函數(shù)體里的內(nèi)容,此時func函數(shù)會形成一個新的私有作用域,按照之前描述的步驟:
- 先給形參num1、num2賦值,分別為100、200;
- func中的代碼進行預(yù)解析;
- 執(zhí)行func中的代碼
因為在func函數(shù)內(nèi)進行了預(yù)解析,所以func函數(shù)里面的total變量會被預(yù)解析,在函數(shù)內(nèi)第一次輸出total的時候,會輸出undefined,接著為total賦值了,第二次輸出total的時候就輸出300。 因為函數(shù)體內(nèi)有var聲明的變量total,函數(shù)體內(nèi)的輸出total并不是全局作用域中的total。
最后一次輸出total的時候,輸出0,這里輸出的是全局作用域中的total。
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);
將代碼作小小的變形之后,func函數(shù)體內(nèi)的total并沒有使用var聲明,所以total不是私有的,會到全局作用域中尋找total,也就說說這里出現(xiàn)的所有total其實都是全局作用域下的。
四、 全局作用域下帶var和不帶var的區(qū)別
在全局作用域中聲明變量帶var可以進行預(yù)解析,所以在賦值的前面執(zhí)行不會報錯;聲明變量的時候不帶var的時候,不能進行預(yù)解析,所以在賦值的前面執(zhí)行會報錯。
console.log(num1); var num1 = 12; console.log(num2); num2 = 12;
輸出結(jié)果.png
num2 = 12;
相當(dāng)于給window增加了一個num2的屬性名,屬性值是12;
var num1 = 12;
相當(dāng)于給全局作用域增加了一個全局變量num1,但是不僅如此,它也相當(dāng)于給window增加了一個屬性名num,屬性值是12;
問題:在私有作用域中出現(xiàn)一個變量,不是私有的,則往上級作用域進行查找,上級沒有則繼續(xù)向上查找,一直找到window為止,如果window也沒有呢?
獲取值:console.log(total);
--> 報錯 Uncaught ReferenceError: total is not defined
設(shè)置值:total= 100;
--> 相當(dāng)于給window增加了一個屬性名total,屬性值是100
function fn() { // console.log(total); // Uncaught ReferenceError: total is not defined total = 100; } fn(); console.log(total);
注意:JS中,如果在不進行任何特殊處理的情況下,上面的代碼報錯,下面的代碼都不再執(zhí)行了
五、 預(yù)解析中的一些變態(tài)機制
5.1 不管條件是否成立,都要把帶var的進行提前的聲明
if (!('num' in window)) { var num = 12; } console.log(num); // undefined
JavaScript進行預(yù)解析的時候,會忽略所有if條件,因為在ES6之前并沒有塊級作用域的概念。本例中會先將num預(yù)解析,而預(yù)解析會將該變量添加到window中,作為window的一個屬性。那么 'num' in window 就返回true,取反之后為false,這時代碼執(zhí)行不會進入if塊里面,num也就沒有被賦值,最后console.log(num)
輸出為undefined。
5.2 只預(yù)解析“=”左邊的,右邊的是指,不參與預(yù)解析
fn(); // -> undefined(); // Uncaught TypeError: fn is not a function var fn = function () { console.log('ok'); } fn(); -> 'ok' function fn() { console.log('ok'); } fn(); -> 'ok'
建議:聲明變量的時候盡量使用var fn = ...的方式。
5.3 自執(zhí)行函數(shù):定義和執(zhí)行一起完成
(function (num) { console.log(num); })(100);
自治性函數(shù)定義的那個function在全局作用域下不進行預(yù)解析,當(dāng)代碼執(zhí)行到這個位置的時候,定義和執(zhí)行一起完成了。
補充:其他定義自執(zhí)行函數(shù)的方式
~ function (num) {}(100) + function (num) {}(100) - function (num) {}(100) ! function (num) {}(100)
5.4 return下的代碼依然會進行預(yù)解析
function fn() { console.log(num); // -> undefined return function () { }; var num = 100; } fn();
函數(shù)體中return下面的代碼,雖然不再執(zhí)行了,但是需要進行預(yù)解析,return中的代碼,都是我們的返回值,所以不進行預(yù)解析。
5.5 名字已經(jīng)聲明過了,不需要重新的聲明,但是需要重新的賦值
var fn = 13; function fn() { console.log('ok'); } fn(); // Uncaught TypeError: fn is not a function
經(jīng)典題目
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();
總結(jié)
以上就是關(guān)于JavaScript中預(yù)解析的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。
相關(guān)文章
微信小程序使用onreachBottom實現(xiàn)頁面觸底加載及分頁效果
小程序還沒有使用pc端的那種分頁格式,下面這篇文章主要給大家介紹了關(guān)于微信小程序使用onreachBottom實現(xiàn)頁面觸底加載及分頁效果的相關(guān)資料,需要的朋友可以參考下2022-10-10微信小程序項目實踐之九宮格實現(xiàn)及item跳轉(zhuǎn)功能
這篇文章主要介紹了微信小程序項目實踐之九宮格實現(xiàn)及item跳轉(zhuǎn)功能,需要的朋友可以參考下2018-07-07JavaScript判斷數(shù)據(jù)類型有幾種方法及區(qū)別介紹
這篇文章主要介紹了JavaScript判斷數(shù)據(jù)類型有幾種方法及區(qū)別介紹,本文給大家分享多種方法通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09js中flexible.js實現(xiàn)淘寶彈性布局方案
這篇文章主要為大家詳細介紹了js中flexible.js實現(xiàn)淘寶彈性布局方案,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2015-12-12關(guān)于原生js中bind函數(shù)的簡單實現(xiàn)
下面小編就為大家?guī)硪黄P(guān)于原生js中bind函數(shù)的簡單實現(xiàn)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08