函數: 封裝代碼實現某個功能,最初思路解決代碼重復度高的問題,類似于變量(就是一個筐,往里面填充內容即可) 【編程思想:高內聚、低耦合】【聚焦點:參數和返回值,后續就是函數的嵌套,才延生出作用域、閉包等問題,然后尋求解決方案】
1、函數的基本認知:
(1).函數聲明、調用:function test(){};? test:?函數名/函數引用;?test();?函數執行;[function/var 都是關鍵字; 函數名命名規則:小駝峰規則(并非是語法規則,而是開發規范)]【functiontheFirstName(){} ?document.write(theFirstName);// 打印出函數體,絕對不是地址,區別于編譯性語言,解釋性語言是打印不出地址的】
(2).函數表達式:指代的便是匿名函數表達式;
(3).參數的使用:使函數功能更加豐富;[形參==>?函數內隱式var了多個變量,形參和實參都是變量,它們可以是變量的任何類型:原始值/引用值,取決于函數封裝的代碼要實現的功能]
參數個數問題、實參列表;[實參與形參傳遞時最好要相互對應,要不后續會遇到函數內部封裝好的對象,event/error等的使用,避免出錯]??
映射規則、特殊性;[arguments:每個函數內部系統都會隱式定義一個實參列表,實參都有地方存放,console.log(arguments)]
例子:
(4).return(函數結束條件及返回值,如果不寫 -> 函數末尾隱式添加 return undefined;)
(5).作用域:全局變量/局部變量的訪問規則:
嵌套函數:里面可訪問外面,外面不可訪問里面;兩個相互獨立的函數也不可互相訪問;
補充:定義函數的方式:
[1].函數聲明;[2].函數表達式(匿名函數表達式);[3].使用對象Function,類似Number/Boolean/String/Array/Object;(了解即可)
[4].函數在對象中的使用,稱為方法;[5].匿名函數function(){},箭頭函數() => { };?這兩種一般作為回調函數使用(作為其他函數的參數),或者當作立即執行函數使用;
箭頭函數:() => {};? =>?表示函數中的大括號,{ }?函數體;( )函數參數,當函數只有一個形參()可省略,當函數體只有一條語句時,{}可省略,若有return,return單詞也可省略;最好都別省略;
2、遞歸:有很強抽象規律使用,階乘、斐波那契數列是典型的兩個案例;[遞歸可以使代碼更加簡捷,缺點:運行速度較慢,往往有很強抽象規律才使用]
?【1.找規律;2.找出口,防止進入死循環】
//使用過程中,只需要在函數體內寫入:return 公式? 就OK;(盡量做到靈活使用,但其確實不容易想到,后續有些地方會用到遞歸的思想,例如:算法)
3、預編譯:解決代碼執行順序問題;【聚焦點:GO全局形成、AO函數執行前形成,聚焦在變量】
js運行三部曲:?語法(語義)分析、預編譯、解釋執行;
(1).語法分析:檢查代碼是否有低級錯誤,若出現低級語法錯誤,則不執行;[關于錯誤后還有更文]
(2).預編譯:代碼執行前一刻需要進行預編譯,全局的預編譯/函數執行前的預編譯;[日常開發編寫代碼是視覺效果展示,計算機執行過程是遵循預編譯的順序執行,預編譯生成的對象就是一個空間存儲庫]
(3).解釋執行;
window全局對象的整體認知:【補充:undefined:聲明了但沒值,例如console.log(window.a);其也是對象中很特殊的一類現象,a is not defined;?壓根就不存在該變量】
預編譯:變量聲明提升、函數聲明整體提升都只是預編譯中的現象而已;一般遇到代碼執行問題,上述兩條就可以解決,不容易解決時創建GO、AO對象分析;[GO對象就是window對象,表示全局]
[定義變量=變量聲明+變量初始化(賦值)? var a; //變量聲明? ? a = 10; //變量初始化? ?var a=10; //定義變量?]
先形成GO對象,再形成AO對象;[代碼包含無非就是:變量、函數、語句,GO對象是在全局時候形成的,AO對象是函數執行前一刻形成的,一定要善于區分,函數輸出的變量是全局還是AO]
升華例子1:函數執行前形成的AO對象中肯定會有變量b的聲明,即使函數中該變量在if條件判斷語句中,預編譯過程中形成的對象就是后續代碼執行的依據,符合條件的提升就OK;
升華例子2:關于代碼執行順序可以get到一些點(例子1顯然返回函數、例子2因為有賦值,所以返回的是值),總之靈活應對;
【補充:如上console.log(bar);//輸出整個函數;bar:函數名/函數引用;console.log(bar()); ?- - - > 輸出函數執行結果】
----------->>>>>>生成的GO/AO對象放到什么地方?函數中里面的可訪問外面的,外面的卻不可訪問里面的這類現象的底層邏輯是什么?[之前查找變量是從GO/AO對象中查找,底層實際是從函數的作用域鏈中去查找]
4、作用域、作用域鏈精解:【聚焦點是:函數嵌套時變量的查找】
【window:全局的域;函數都有自己的獨立作用域】
每個函數都有自己的作用域鏈;產生的執行期上下文的集合形成了作用域鏈中,訪問變量的順序也遵循該鏈;里面可訪問外面,外面無法訪問里面;
一個函數只有執行的時候,產生AO對象,系統才能看到里面的東西,否則定義階段根本就看不到里面的東西;只有執行的時候才開始讀取函數里面的內容;
(1).函數也是對象(函數類對象):對象都有屬性,有些屬性我們可以直接訪問test.name/prototype;但有些屬性是不可直接進行訪問,僅供javaScript引擎讀取,[[scope]]就是其中的一個,test[[scope]];
[[scope]]:每個函數都有該屬性,稱為作用域,其內部存儲了運行期上下文的集合,這個集合呈鏈式鏈接,就是作用域鏈;
運行期上下文:也稱為執行期上下文,當函數執行的前一刻,會創建執行期上下文的內部對象,也就是預編譯時產生的GO/AO對象;[函數每次執行時都會創建新的執行期上下文(對象),所以多次調用一個函數會創建多個執行期上下文,當函數執行完畢后,所產生的執行期上下文被銷毀,這里的銷毀指代的是引用銷毀,而非代碼直接銷毀]
查找變量:從作用域鏈的頂端依次向下查找;
(2).過程剖析:
[1].函數定義的過程中函數的作用域便已經存在,即[[scope]]已經存在;首先生成GO對象,這時候a函數被定義,定義的過程中作用域鏈中 0 -->> GO對象;[函數定義的階段(GO對象生成的過程),系統根本不會關注函數體里面的內容,不會解析,生成AO對象才開始分析函數體里面的內容]
[2].GO對象形成后,開始執行代碼----->>a();--->>>函數執行前一刻形成AO對象,此時的作用域鏈中0 --->> AO對象;? 1 --->>GO對象;新形成的AO對象會在作用域鏈的頂端;[執行過程中查找變量肯定是在作用鏈中去查找,先找自己AO對象,再查找GO對象,即從作用鏈頂端以此向下查找]
[3].a函數的執行過程中,b函數開始定義(預編譯過程);定義的時候b函數會產生自己的[[scope]],其的作用域鏈環境是a函數給的,也就是a函數執行時候的環境;
[4].b函數執行中會產生自己的AO對象,然后放置到自己作用域鏈的最頂端;
[5].b函數執行結束后,銷毀自己的執行期上下文對象即AO對象(回歸到被定義的狀態,如果有二次執行,再形成新的AO對象,執行的過程中創建AO對象,執行結束后銷毀相應的AO對象即可)所謂的銷毀并非是直接把AO對象抹掉了,而是把線剪斷;[b函數作用域鏈中拿到的a函數執行過程中的作用域鏈,它們指向是相同的,只不過是生成自己的AO對象而已]
[函數內查找變量的過程:在函數自己的作用域鏈中自頂向下找,里層函數可訪問外層函數的變量,但外層函數絕對不可能訪問到里層函數的變量,外層函數作用域鏈中自己的AO與外層的環境,自己的AO對象只有里層函數的定義,定義階段系統又不解析函數體內部,所以壓根找不到函數里面的變量]
[6].b函數執行結束后,代碼中a函數也執行結束了,a函數需要銷毀自己的執行期上下文對象,回歸到被定義的狀態,銷毀AO對象的同時,b函數定義也被銷毀了,也就是b函數徹底結束了,作用域鏈中沒東西了;
[7].若是a函數再次執行,需要從新開始,創建AO對象等一系列操作同上;若是函數內嵌套了多個函數,最里面的函數作用鏈會很長,當最外面函數執行完成后,里面的函數連定義狀態都沒有了,作用鏈為空; ---->>>>>?但有特殊情況,函數執行完成后卻把里面的函數return出去了,形成閉包;
5、閉包:其是一類現象;函數嵌套中外部函數結束執行時,內部函數由于被return到了外部,依舊存活,內部函數生命周期比外部函數還長;【內部函數被保存到外部就會形成閉包,但這個過程很容易被忽視】
過程分析:[1].a函數結束執行后,return b;b函數依舊屬于被定義的狀態,環境是a函數執行時的作用域鏈,這時候b函數依舊可以操作外面函數的變量(作用域鏈的原因);[若是只是普通的在a函數外部定義的函數,由于作用域的原因,其必然是不能訪問a函數中的變量的,但閉包可以]
閉包:函數執行結束會釋放作用域鏈占用的空間,但若是函數嵌套多層,形成閉包則會導致原有作用域鏈不釋放,占用內存空間(也稱為內存泄漏【現象一致:都是占用內存,剩余內存少了】);閉包有壞處需要防治,但也有很好的應用場景來實現一些功能;
閉包的作用:
(1).實現公有變量:函數累加器;[return demo;demo的作用域鏈是add函數執行時的環境,指向的就是同一套,demo();每次指向環境沒變只是創建了新AO對象而已,所以每次操作都會改變原來的值]
(2).可以做緩存;(緩存就是存儲結構)
(3).可以實現封裝、屬性私有化;
(4).模塊化開發、防止污染全局變量;,
閉包的bug以及解決方案:[下式是經常會遇到的一種閉包情況,高頻觸發,經常會被忽視]?考慮過程中需要結合預編譯、作用域鏈、閉包的知識,開始寫GO/AO對象分析,后續盡量抽象化去分析;? ? ? myArr = [function (){console.log(i} ......] //里面10個相同的函數;myArr[j]();這時候才開始執行函數,i自身AO對象中沒有,沿著作用域鏈找test中有,此時i為10;【里面函數與test()?形成10對1的閉包】
閉包的解決方案:立即執行函數(其也是唯一的解決方案);[每循環一次立即執行函數就執行,過后銷毀,但銷毀并非是把里面的代碼給抹掉了,只是把引用抹掉了而已,循環了10次,有10個不同的立即執行函數] [立即執行函數雖然銷毀,但最終把里面的函數弄了出來,其和立即執行函數形成了閉包,j自身沒有就從立即執行函數中查找] 【里面函數與立即執行函數形成:1對1的閉包】
6.立即執行函數(也稱為函數的自執行):該函數執行過后就銷毀便不存在了;[js中提供立即執行函數來處理那些只執行一次的函數,有些函數內容巨長會占用內存][所謂的銷毀并不是直接把代碼抹掉了,而是把函數的引用抹掉]
[無論如何有的函數只執行一次(只被執行一次,我們想要其執行一次,或者執行一次返回結果的函數),我們稱之為初始化函數或初始化功能,例如開發中的數據進行初始化等,初始化后該函數就沒用了]
常規用法:[立即執行函數沒有函數名,函數名沒什么用處,寫上倒也不會報錯]
拓展(深挖底層原理):只有表達式才能被執行符號執行,執行后基本就是立即執行函數了; [function test(){}();//報錯; test();不報錯,test代表著函數體,為什么兩者結果不同??原因:test也是表達式,例如123是數字表達式];
var test = function(){ console.log('a');}();//表達式加上執行符號,執行過后就銷毀console.log(test);//undefined;
回歸到最初所提到的:(function (){} ()); (function (){})();這兩種立即執行函數形式,可以理解為外面的括號將函數聲明變為了表達式,然后被執行符號執行,變為立即執行函數;
升華例子:括號首先把函數聲明變為了表達式,然后執行if條件判斷;(開發中也沒有人這么寫)