摘要:當我們把函數傳遞給函數,并且讓能夠在某一時刻執行,這種情況我們稱函數是回調函數,簡稱回調。返回函數剛剛在回調函數部分,說的是函數作為另一個函數的參數傳遞,接下來說說函數作為另一邊函數的結果返回。
寫在前面
由于詞語匱乏,本文繼續沿用"深入解析xxx"這個俗套的命名,但是是真的很深入(你要信我啊)。如果本文對你有用,歡迎收藏,如果喜歡我的文章,歡迎點贊和關注專欄。
函數可以說是js的基礎,無處不在,功能又十分強大,本文將簡單介紹函數的特點并且重點介紹各種各樣的用法。廢話不多說,開車~
友情提示,由于本文涵蓋的內容比較全面,不免篇幅稍長,中途請注意休息。
但是其實,函數的本質就是對象。確切一點來說,其實是第一類對象(first-class object)。關于第一類對象,wiki解釋如下:
第一類對象又稱第一類公民,在編程語言中指的是一個具有以下特性的實體:
能夠作為參數被傳遞
能夠從一個函數結果中返回
能夠被修改和賦值給變量
雖然看起來高大上,但是我們只要先記住,在js里函數也是對象,可以擁有自己的屬性和方法,而它和一般js對象的區別是:可以被調用,也就是可執行。
當然,函數還有一個明顯的特點就是,提供作用域:在函數作用域內的變量都是局部變量,對外部不可見。由于js中其他代碼塊,比如for和while循環等并不提供作用域,所以有很多地方會利用函數來控制作用域。在后面會一一提到。
預備知識這一塊在之前講閉包的時候其實提到了一些,但是還是簡單介紹下。
函數作用域在類似C語言的編程語言中,花括號{}表示一個作用域:在作用域內的變量對外不可見,這個稱為塊級作用域,但是在js中沒有塊級作用域,只有函數作用域:在函數體內聲明的變量,在整個函數體內有定義。
function fun(){ for(var j =1;j<10;j++){ } console.log(j)//10 } console.log(j)//undefined
這個例子中變量j定義在函數體中,那么在函數體內可以訪問,在外部則無法訪問。
作用域鏈作用域鏈,就是一個類似鏈表的解構,它表示當前代碼有權訪問的作用域的訪問順序。舉個例子:
var a = 1; function fun(){ var a = 2 console.log(a) } fun()//2
在這里,執行fun()時,作用域鏈上有2個作用域,第一個是fun,第二個是全局環境,按照順序,首先訪問內容的作用域,找到了a變量,那么就不繼續尋找,如果這里沒有var a = 2,那么會繼續向外尋找,最終輸出的就是1。
只要記住,作用域鏈都是從當前函數作用域向外一層層延伸的,所以內部作用域可以訪問外部變量,反之則不行。
聲明提升看下這個例子:
function fun(){ console.log(a) var a = 1; } fun();//underfined
是不是覺得很奇怪,這里既沒有未定義報錯,也沒有輸出1,因為這里的代碼其實相當于這樣寫:
function fun(){ var a; console.log(a) a = 1; } fun();//underfined
可以看到,其實變量a的聲明,相當于被提前到當前函數作用域的頂部,這就是所謂的聲明提升,但是要注意,聲明雖然提升了,賦值a=1并沒有被提升,否則這個例子應該直接輸出1。
接下來再舉1個例子回顧下這一階段的知識:
var a = 1; var b = 4; function fun (){ console.log(a); var a = 2; var b = 3; console.log(b); } fun (); console.log(b);
具體結果大家可以跑跑看。
函數的創建通常來說,有2種創建函數的方式:函數表達式、函數聲明。
函數表達式函數表達式通常具有如下形式:
var funA = function funName(param1,param2){ //函數體 }
當然,更常見來說這里的funName是不寫的,寫與不寫的區別是,在不同瀏覽器中,獲得的函數對象中name屬性的值會被處理成不行的形式。
//這個例子可以在ie firefox webkit內核的瀏覽器分別跑一下看看結果 var fun1 = function(){} var fun2 = function funName(){} console.log(fun1) console.log(fun2)
寫函數名字有個比較好用的地方是在遞歸的時候,可以很方便使用:
//階乘函數 var fun1 = function recu(x){ if(x<=1) return 1; else return x*recu(x-1) }函數聲明
函數聲明形式一般如下:
function funName(){ //函數體 }
這個和函數表達式的區別就是,使用函數聲明的方式在js里會有"提升",而使用表達式方式寫沒有提升所以函數表達式定義的函數無法提前使用
fun1();//fun1 fun2();//報錯 function fun1 (){ console.log("fun1") } var fun2 = function(){ console.log("fun2") }
因為前面說過,賦值部分不會提升,而函數表達式的寫法本質上也是一個變量聲明和賦值,形如var x = function...,x的聲明被提升,但是右邊的賦值部分要等待代碼執行到這句的時候才生效。
舉個更容易理解的例子:
console.log(fun2)//underfined fun2();//報錯 var fun2 = function(){ console.log("fun2") }
同理,變量fun2已聲明,但未賦值。所以這里console.log的時候不報錯,運行的時候才報錯。看不懂請再回顧下預備知識的聲明提升部分。
函數參數函數的參數一般分成形參和實參,形參是函數定義時預期傳入的參數,實參是函數調用時實際傳入參數。
參數數量不對等情況和argumentsJavascript沒有在函數調用時對實參做任何檢查。 所以可能出現以下情況:
當傳入的實參比形參個數要少的時候,剩下的形參會被自動設置為underfined,所以在寫函數的時候,我們經常要注意是否要給參數一些默認值
function fun(a){ var a = a || "" //如果傳入a就使用a,否則a設置為空字符串 }
如果我們的函數使用了可選參數,那么可選參數的位置必須放在最后,否則,使用者調用時候,就要顯式傳入underfind,比如fun(underfined,a)表示第一個參數不傳入。
當傳入的實參比形參個數要多的時候,我們可以通過標識符arguments對象來獲得參數
function fun(a){ if(arguments.length>1)console.log(arguments[1])}; var a=1,b=2; fun(a,b);//2
這個例子中,通過arguments輸出了實參b的值。值得一提的是,arguments并不是數組,而是一個對象,只是恰好使用數字為索引
callee 和 calleres5的非嚴格模式下,我們可以使用callee 和 caller這兩個屬性,
callee 表示當前正在執行的函數,通常用法是在匿名函數中寫遞歸調用
caller 表示調用當前正在執行函數的函數,可以用來訪問調用棧,這個屬性是非標準的,但是大部分的瀏覽器都實現。更詳細的用法可以查看MDN。
函數的模式模式其實就是函數的各種應用方式,也是本文的重點
api模式api模式主要是給函數提供更好的接口。
回調模式最前面已經提到,函數是對象,并且可以被作為參數傳遞給其他的函數。
當我們把函數A傳遞給函數B,并且讓B能夠在某一時刻執行A,這種情況我們稱函數A是回調函數(callback function),簡稱回調。
舉個例子,假設這樣一個背景:假設現在我們需要處理一批dom節點,處理大概分2步,第一步,篩選出符合要求的一部分節點,第二步,對這部分數據做一些css樣式修改。那我們一般會先想到這樣寫:
//篩選函數 function filterNodes(nodes){ var i = 0; var result = []; for(i = 0; i按照上面定義的2個函數,先用filterNodes篩選符合要求額節點,然后將結果作為operate函數的參數,這樣邏輯上是完全沒問題的,只是有一個地方:其實我們已經2次遍歷了符合要求的節點:第一次是在篩選時,第二次是在樣式操作時。這里有辦法優化嗎?,如果我們直接把樣式操作直接寫到result.push()后面,是可以減少一次遍歷的,但是這樣filterNodes函數就不是一個純粹的篩選節點的數了。所以我們可以使用回調模式來解決,只需稍微修改下:
//篩選函數 function filterNodes(nodes,callback){ var i = 0; var result = []; for(i = 0; i這樣改造之后,2個函數依然各自擁有自己的邏輯,而且我們可以通過調用filterNodes時,傳遞不同參數的辦法,來控制我們想要的功能。
回調函數還有很多的常見用途:
異步事件監聽
最常見的例子莫過于我們為文檔添加監聽事件:document.addEventListener("click",[回調函數],false)有了回調模式以后,程序可以以異步的模式運行:只有用戶觸發了某些交互行為,才會調用到我們指定的函數。
超時方法 setTimeout()和 setTimeInterval()
這兩個函數也一樣接受回調函數setTimeout([回調函數],200)軟件庫設計
返回函數
設計一個庫的時候,很重要的就是設計通用性和復用性的代碼,因為無法提前預測到需要的每一個功能,而且用戶也不會總是需要用到所有的功能,利用回調模式,很容易設計出具有核心功能有同時提供自選項的函數(比如前面提到的節點篩選函數,核心功能是篩選,又能根據需要插入后續操作)。剛剛在回調函數部分,說的是函數作為另一個函數的參數傳遞,接下來說說函數作為另一邊函數的結果返回。看下面一個計時器例子:
var counter = function(){ var count = 0; return function(){ return count++ } } var f = counter(); f();//1 f();//2其實這里就是一個閉包的實例,關于閉包,在我的另一篇文章里有更詳細的描述點擊前往
配置對象配置對象模式其實就是讓用對象作為函數的參數。
這種模式經常用在建立一個庫,或者寫的函數要提供給外部調用時。因為它能提供很簡潔的接口。假設這樣一個例子:function operate(para1,para2){}如果我們正在寫一個庫函數,一開始我們預料到的參數只會有para1,para2,但是隨著不斷拓展,后來參數變多了,而且出現了一些可選參數para3,para4:
function operate(para1,para2,para3,para4...)此時我們需要很小心的把可選參數放在后面,使用者在調用的時候還必須很小心的對上位置,比如說:
operate(p1,p2,null,p4)//這里的null不可省略此時,參數數量太多,使用起來需要很小心記住參數順序,很不方便。所以就要采用配置對象的寫法,即把參數寫成一個對象:
function operate(config){} var conf = { para1:..., para2:..., para4:..., } operate(con)這樣的寫法
優點是:使用者不需要記住參數順序,代碼也顯得更簡潔,
缺點是:使用時要嚴格記住參數的名稱,并且屬性名稱無法被壓縮
通常在操作dom對象的css樣式時候會用這樣的寫法,因為css樣式有很多,但是名稱很容易記住,比如
var style ={ color:"..." border:"..." }柯里化start18/08/08編輯
柯里化內容已添加,傳送門
end18/08/08編輯
柯里化的內容比較長,難度也稍大,后續另開一篇來寫吧~~。
初始化模式初始化模式的主要作用是不污染全局命名空間,使用臨時變量來完成初始化任務,使任務更加簡潔
即時函數即時函數模式(immeddiate Function pattern),是一種支持在定義函數后立即執行該函數的語法。也叫作自調用和自執行函數(function(){ //函數內容 }()) //也可以這樣寫 (function(){ //函數內容 })()這里給出了即時函數的兩種寫法,它的作用是可以給初始化的代碼提供一個存放的空間:比如在頁面初始化時,需要一些臨時變量來完成一次初始化,但是這些工作只需要執行一次,執行之后就不再需要這些臨時變量,那么我們就不必浪費全局變量來創建這些變量,此時使用即時函數,可以把所有代碼打包起來,并且不會泄露到全局作用域。比如:
(function(){ var initName = "" alert(initName) }());當然,即時函數也可以傳遞參數,
(function(initName){ alert(initName) }("hello"));同樣也可以有返回值:
var result = (function(){ return 1 }()); console.log(result)//1即時函數經常用在寫一些自包含模塊,這樣的好處是可以確保頁面在有無該模塊的情況下都能良好運行,很方便的可以分離出來,用于測試或者實現,或者根據需要實現“禁用”功能。例如:
//moudle1.js (function(){ //模塊代碼 }//)按照這一的形式寫模塊。可以根據需要加載模塊。
即時對象初始化這個模式和即使函數模式很相似,區別在于我們的函數寫在一個對象的方法上。通常我們在一個對象上寫上init方法,并且在創建對象之后立即執行該方法。如下:
({ //初始化的屬性和配置 name:"Mike", age:"12", //其他方法 ... //初始化 init:function(){ ... } }).init();這個語法其實相當于在創建一個普通的對象并且,然后在創建之后立刻調用init方法。這種做法和即時函數的目的是一致的:在執行一次性初始化任務時保護全局命名空間。但是可以寫出更加復雜的結構,比如私有方法等,而在即時函數里面只能把所有的方法都寫成函數。
初始化時分支初始化時分支經常用在某個生命周期中做一次性測試的情境中。所謂的一次性測試就是:在本次生命周期中,某些屬性不可能改變,比如瀏覽器內核等。典型的例子是瀏覽器嗅探.
看過javacscript高級程序設計的話,對這個例子一定很眼熟:
var utils = { addListener:function(el,type,fn){ if(typeof window.addEvenrtListener === "function"){ el.addEventerListener(type,fn,false); } else if(typeof window.attachEvent === "function"){ //ie el.attachEvent("on" + type,fn) } else{ //其他瀏覽器 el.["on"+ type] = fn } } ...//刪除方法類似 }這個例子是為了寫一個能夠支持跨瀏覽器處理事件的方法,但是有個缺點:每次在處理事件時都要檢測一次瀏覽器的類型。我們知道,其實在一次頁面的生命周期里,其實只需要檢測一次就夠了,所以可以利用初始化分支來這樣改寫:
var utils = { addListener:null } if(typeof window.addEvenrtListener === "function"){ utils.addListener = function(el,type,fn){ el.addEventerListener(type,fn,false); } } else if(typeof window.attachEvent === "function"){ //ie utils.addListener = function(el,type,fn){ el.attachEvent("on" + type,fn) } } else{ //其他瀏覽器 utils.addListener = function(el,type,fn){ el.["on"+ type] = fn } }這樣的話就可以在加載時完成一次嗅探。
性能模式性能模式,主要是在某些情況下加快代碼的運行。
備忘模式備忘模式的核心是使用函數屬性,緩存能計算結果。以便后續調用時可以不必重新計算。
這么做的基礎主要是之前提到過的,函數本質還是對象(這句話已經重復n次了),既然是對象自然可以擁有屬性和方法,例子:var fun = function(key){ if(!fun.cache[key]){ //不存在對應緩存,那么計算 var result = {} ...//計算過程 fun.cache[key] = result } return fun.cache[key] }這里舉了一個比較簡單的例子,在獲取對應數據的時候,先判斷有無緩存,有的話直接獲取;沒有的話計算一次并緩存到對應位置。之后便無需重復計算。
當然,這里的key我們假設是基本類型的值,如果是復雜類型的值,需要先序列化。
自定義模式
另外,在函數內的fun可以通過前面提到的arguments.callee來代替,只要不在es5的嚴格模式下就行。自定義函數的原理很簡單:首先創建一個函數并保存到一個變量f。然后在創建一個新函數,也保存在這個變量f,那么f最終指向的應該是新的函數。那么如果我們讓這個過程發生在舊的函數內部,那么就實現了惰性函數。話不多說,看例子:
var fun = function(){ console.log("在這里執行一些初始化工作") fun = function(){ console.log("在這里執行正常工作時需要執行的工作") } } fun();//在這里執行一些初始化工作 fun();//在這里執行正常工作時需要執行的工作 fun();//在這里執行正常工作時需要執行的工作在這里我們執行了一次初始化任務以后,函數就變成了正常的函數,之后的執行就可以減少工作。
總結這是2018年寫的第一篇長文(其實一共就寫了2篇,哈哈哈)希望今年自己可以好好努力,把“深入”系列貫徹到底。也希望大家都有所進步。
然后依然是每次都一樣的結尾,如果內容有錯誤的地方歡迎指出;如果對你有幫助,歡迎點贊和收藏,轉載請征得同意后著明出處,如果有問題也歡迎私信交流,主頁添加了郵箱地址~溜了
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92411.html
摘要:模板解析器原理本文來自深入淺出模板編譯原理篇的第九章,主要講述了如何將模板解析成,這一章的內容是全書最復雜且燒腦的章節。循環模板的偽代碼如下截取模板字符串并觸發鉤子函數為了方便理解,我們手動模擬解析器的解析過程。 Vue.js 模板解析器原理 本文來自《深入淺出Vue.js》模板編譯原理篇的第九章,主要講述了如何將模板解析成AST,這一章的內容是全書最復雜且燒腦的章節。本文未經排版,真...
摘要:預解析聲明告知瀏覽器在全局作用域中有一個變量名為的變量。執行代碼的就是棧內存,作用域也是棧內存。關鍵字在中主要研究都是函數中的中的代表的是當前行為執行的主體方法,函數,事件中的上下文代表的是當前行為執行的環境區域例如小明在沙縣小吃吃蛋炒飯。 基本認識 數據類型 基本數據類型 string, number, null, boolean, undefined 引用數據類型 object: ...
摘要:在高級的技巧中會用來創建作用域安全的構造函數。運算符希望左操作數是一個對象,右操作數表示對象的類。中對象的類似通過初始化它們的構造函數來定義的。為了理解運算符是如何工作的,必須首先理解原型鏈原型鏈可作為的繼承機制。 在js高級的技巧中會用instanceof來創建作用域安全的構造函數。instanceof運算符希望左操作數是一個對象,右操作數表示對象的類。如果左側的對象是右側類的...
摘要:本計劃一共期,每期重點攻克一個面試重難點,如果你還不了解本進階計劃,點擊查看前端進階的破冰之旅本期推薦文章深入之執行上下文棧和深入之變量對象,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第一期,本周的主題是調用堆棧,今天是第二天。 本計劃一共28期,每期...
摘要:下面是用實現轉成抽象語法樹如下還支持繼承以下是轉換結果最終的結果還是代碼,其中包含庫中的一些函數。可以使用新的易于使用的類定義,但是它仍然會創建構造函數和分配原型。 這是專門探索 JavaScript 及其所構建的組件的系列文章的第 15 篇。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 如果你錯過了前面的章節,可以在這里找到它們: JavaScript 是...
閱讀 3288·2021-09-08 09:45
閱讀 1251·2019-08-30 15:53
閱讀 1522·2019-08-30 14:12
閱讀 981·2019-08-29 17:01
閱讀 2568·2019-08-29 15:35
閱讀 394·2019-08-29 13:09
閱讀 1965·2019-08-29 12:32
閱讀 3083·2019-08-26 18:37