前端--關于javascript函數

yunjian 8年前發布 | 17K 次閱讀 JavaScript開發 JavaScript

來自: http://www.cnblogs.com/jinjilin/p/5164233.html

終于可以說說函數了,函數是javascript設計最出色的地方,可以說它是所有概念中最重要的一個,因為圍繞函數而闡述的周邊概念涵蓋了javascript的方方面面,所以理解了函數可以說對javascript有了大半部分的理解,在此我只能懷著謹慎與謙遜的態度小心翼翼的總結出自己所理解的函數。

  • 函數是什么

要理解javascript函數首先得弄明白它是什么,這個問題并不是很容易就讓人接受的,尤其是對有過C語言或者java等編程經驗的人來說,因為會受到函數聲明寫法的誘惑,認知更傾向于把它理解為一種編程方式,一種可以重復調用執行的具名語句塊。而在javascript中,函數是一種數據類型不是一種編程方式,它是對象類型的實例,它就是對象,它就是一種值,它和{x:3}這種對象直接量的地位是一樣的。這種對象的定義有它自己特殊的寫法,解釋器在進行詞法分析的時候會對此進行判斷,它的可重復執行的語句塊只是這個對象的一個隱藏屬性,而要調用這個隱藏屬性的執行只需要用到該對象加()運算符即可。

  • 函數定義

函數定義有三中方法:定義表達式、函數聲明語句、Function構造函數。

定義表達式從字面上就可以看出它是一個表達式,塔僅僅返回一個函數對象值,主要用在給變量賦值等需要用到值的地方,它的寫法和函數聲明語句是差不多的,都是function關鍵字+標識符+()+{},只是這個標識符可以省略,省略標識符的函數也叫做匿名函數,不省略的話就只是在函數體內可以訪問的一個指向函數本身的局部變量,一般情況下是省略的。

而函數聲明語句就相當于一條賦值語句,可以看作是把變量聲明與函數對象值的賦值合并在一起的寫法,所以function后面的標識符是不能省略的,并且這個標識符在函數體外面可以訪問到。但函數聲明語句畢竟是有別于一般的函數賦值語句的,最重要的一條區別就是變量聲明提前。函數聲明語句在程序執行到函數聲明之前時是可以調用該函數的,例如fun1();function fun1(){};這樣在聲明fun1之前調用fun1是不會報錯的。函數賦值語句則不行,函數賦值語句要在給變量賦值之后才能調用該變量,否則會報錯,例如fun2(); var fun2=function (){};這樣提前調用就會報錯。除此之外聲明語句和定義表達式還有一個不同就是他倆可以出現的地方:函數聲明語句是不能夠出現在for循環、if/else、try/catch等語句中的,而定義表達式則可以出現在程序的任何地方。

第三種Function構造函數定義函數的方法在實際編程中是不經常使用的,它允許javascript在運行時動態地創建并編譯函數,并且它動態創建的函數中this是指向全局對象的。

  • 函數調用

所謂函數調用就是執行函數對象中隱藏屬性指向的代碼塊,這需要用到一個運算符--(),語法規則就是函數對象+()。

雖然函數調用的語法是唯一的即函數對象+(),但函數調用的模式或者調用場景卻可以有些不同,在javascript中一共有四種調用模式:函數調用、方法調用、構造函數調用、間接調用。這四種調用模式的差別就是函數體中初始化this的取值不同。(this不是變量也不是屬性名,它是一個關鍵字,不能給它賦值)

函數調用很常見也很簡單,就是函數對象+()調用,函數體中的this關鍵字取值初始化為全局對象,要注意的是這里也包括嵌套函數的調用,不要想當然的認為嵌套函數的函數體中this的值會初始化為父函數對象。

方法調用比函數調用多了一步對象屬性的取值過程,因為只有當函數被保存為一個對象的屬性時才叫做方法,函數體中this關鍵字取值初始化為函數屬性所屬的對象。如果一個對象有多個方法,并且每個方法的方法體最后又都返回this,這樣就可以啟用級聯,一個方法可以調用另一個方法形成方法鏈。

構造函數調用就是在函數調用前加一個new關鍵字,構造函數總是會返回一個對象并且函數體中this關鍵字取值初始化為這個返回的對象。這里要注意的是如果是一個方法構造函數,例如 new o.fun(),那么函數體里的this值仍然會被初始化為新建的對象而不是調用對象o。

借用調用也叫做間接調用,這需要用到函數對象從Function.prototype繼承的方法:call、apply、bind,它們可以根據需要指定方法體內this的值。其中apply和call是立即執行原始方法的,只是他倆傳入的參數形式有所不同,例如f.call(o,1,2,3),f.apply(o,[1,2,3]),f中this的值為o傳入的參數是1,2,3;注意apply傳參數的時候不僅僅可以是真實的數組也可以是類數組,比如arguments或者getElementsByTagName。bind方法并不是立即執行的而是返回一個代理函數,例如var a = f.bind(o),這個代理函數a可以在需要的時候再被調用,在調用的時候所有傳遞給a的參數都將傳入原始函數調用執行(這就是為什么它是一個代理函數,它甚至連prototype屬性都沒有)其中的this初始化為先前綁定的對象o。

  • 函數參數

在定義函數的時候可以在括號里寫入形式參數,在函數調用時傳入的實參會初始化相應的形參。這種提前定義的形參其實沒什么卵用,如果非要說有用的話那就是告訴調用者這個函數應該至少傳入幾個參數,因為在javascript中函數調用的時候是不會檢查函數簽名的,不論如何傳遞實參函數體都會執行,而且不管多少個參數都總是會傳入到函數體內。其中的一個原因就是每次調用時都會為函數體傳入兩個隱形參數:this和arguments,this的取值前面已經說過了,arguments是一個類數組對象,它的屬性包括了調用者傳入的所有參數。要訪問傳入的實參值只需要通過arguments加下標就可以,例如arguments[2],這里要注意的是如果定義了形參,那么形參和相應arguments的屬性的關系是別名的關系,就好像引用同一個對象一樣,如果改變了形參值,相應的arguments值也會改變,即便這個值是原始類型。要查看調用者傳入了幾個參數只需要調用arguments.length屬性,這個屬性表示了實參個數,arguments除了具有length屬性,還有callee和caller屬性,callee屬性指向當前運行的函數對象,caller指向調用當前正在執行的函數的函數,只是caller還并不是官方標準。

  • 函數屬性

函數作為一個對象它也是有默認屬性的:length和prototype。函數的length屬性表示當初定義式寫的形參個數,prototype是原型對象,用作構造函數時構造的對象繼承這個prototype屬性值,通過為prototype添加新方法可以拓展內置類型或者原始類型的功能。同時也可以為函數自定義屬性,比如隨便寫一個function a(){a.coun++;},a.count=0,這里就比較有意思了,因為每調用一次a(),a.count就加1,在每次調用中共享的都是同一個a.count,為什么比較有意思呢,因為函數每次調用的時候執行的都是該函數的副本,感覺不應該共享a.count,既然事實上是共享的了,那就說明函數對象a只有一個是不會被復制的,副本指的是代碼塊的副本,并且在每一個代碼塊副本的執行過程中都可以訪問到那同一個函數對象的屬性,這是理解作用域和閉包的一個點。

  • 函數作用域

在一段程序中某個地方用到的變量名并不總是如你所愿可以取到值或者取到你所預測的值,因為在程序中一個變量名字的可用范圍和生命周期是有限的,這個有效的范圍與生命周期就是作用域。根據這種特點可以把變量劃分為全局變量和局部變量,全局變量就是變量名在從程序開始到結束這整個代碼范圍內都是有效的,局部變量只是變量名在整個程序代碼的某一段范圍內是有效的。其他的程序設計語言在一般情況下一個{}括起來的代碼塊就可以劃出一個局部作用域范圍,在{}中聲明的任何變量都只在{}里有效,在{}外面生命周期就停止了,這有個學名叫塊級作用域。然而在javascript中并不支持塊級作用域,也就是說javascript不支持這種劃分局部作用域的模式,javascript有自己特有的劃分局部作用域的模式,可以說javascript只有這一種劃分作用域的模式,就算全局變量也是這種劃分模式的應用。應該知道的是在javascript運行任何代碼之前也就是在javascript解釋器啟動運行的時候(在前端中就是頁面加載的時候),它會創建一個叫做全局對象的對象,這個對象就相當于javascript的類庫,里面有編程時常用的內置方法和屬性,比如Array、Date、parseInt、Math等,同時自己寫的所有源代碼也是這個全局對象的一個隱藏屬性,就好像函數對象的函數體語句塊是函數的一個隱藏屬性一樣,所以可以把這個全局對象看作是一個函數對象,整個javascript源代碼的執行就是調用這個全局函數對象,在這個全局函數對象的函數體中聲明的變量在源代碼中任何地方都是可見的,是全局變量。同樣如果要創建一個局部變量,只需要創建一個函數對象,在這個函數對象的函數體中聲明的變量在函數體執行時只在該函數體內部可見,函數執行結束后在函數體外面訪問不到聲明的變量,所以可以說函數體語句塊是javascript劃分作用域的唯一模式---即函數作用域模式。這種函數作用域比前面的塊級作用域要復雜,塊級作用域的實現是依靠對內存棧的操作,而函數作用域則是函數體內通過遍歷查找函數對象的某個隱藏屬性來實現的。這里要解釋的是在函數對象被定義的時候javascript引擎還為其添加了一些僅供解釋器存取的內部屬性,姑且就叫它[scope]屬性吧,這個隱藏屬性的值是一個對象,這個對象類似于一個數組,下標以0開始,每個“數組“元素都是一個對象,比方說表達式scope[2]返回一個對象,scope[n]返回的對象按鍵值對的形式存儲函數體可以用到的部分變量的名字和值,因此可以說這個scope對象是一個按順序排列的一系列對象的集合,集合中的每個子對象存儲了可用的變量名和變量值,所以這個集合就是常說的作用域鏈。當在函數體內遇到變量名字的時候,解釋器就會按順序遍歷這個集合也就是scope對象(在函數屬性中提到過函數體副本可以訪問函數對象的屬性)來查找同名的對象屬性,找到了就取相應的值找不到就報錯,這個過程叫標識符的解析。

在標識符解析過程之前還存在兩個步驟:scope對象的初始化和“執行環境”對象的初始化。scope對象的初始化過程有一個好聽的名字叫詞法作用域,詞法作用域講的是通過閱讀源代碼看定義變量時的環境就能知道一個變量的作用域,換句話就是說在定義函數的時候scope對象就會被初始化,例如定義一個全局函數function globalFun(){}(這個函數定義在全局環境中),當解釋器掃過這句話的時候會把“全局對象”(這個全局對象代表著所有全局范圍內定義的變量)賦值給globalFun對象的scope[0]。當這個函數被調用執行函數體之前有一個“執行環境”對象初始化的過程,之前說過函數在每次調用的時候執行的都是函數體的副本,解釋器會為每一個副本創建一個唯一的“執行環境”對象,所以如果多次調用會創建多個“執行環境”對象。每個“執行環境”對象的初始化分為兩步:1.復制函數對象的scope屬性值到“執行環境”對象;2.創建一個名字叫做“活動對象”的對象,把這個“活動對象”的屬性名初始化為形參、局部變量名,而屬性值是在不同階段分開賦值的(暫不詳述);并且這個“活動對象”放到這個“執行環境”對象的最前面,函數執行過程中函數體內標識符解析過程用到的作用域鏈都在這個“執行環境”對象里面。

  • 閉包

不少和我一樣的初學者覺得會覺得閉包很難不好理解,我認為主要的原因是這個名字起的太難聽了--!

其實只要明白了閉包產生的原因閉包就很好理解了,產生閉包的原因有兩個:詞法作用域和垃圾回收機制。

前面講了在全局環境下函數作用域鏈的創建,而當在一個函數體環境下聲明一個嵌套函數時情況會有所不同,嵌套函數的scope屬性的會被初始化為引用父函數的“執行環境”對象,所以當嵌套函數被調用的時候它的執行環境對象里包含了父函數的局部變量。javascript中的垃圾回收機制是一種自動的回收,當內存中的對象不存在被引用或者變量不在被使用的時候他們會被js引擎銷毀以釋放內存,所以嵌套的函數被父函數當作結果返回的時候,如果嵌套函數用到了父函數的局部變量,那么在父函數結束調用的時候它的執行環境對象不會被銷毀,它會在嵌套函數的scope屬性值中繼續存在,所以在調用嵌套函數的時候仍然可以訪問到父函數的局部變量。到這我仍的然不知道什么是閉包,有的地方說這個嵌套函數就是閉包,也有的地方說嵌套函數的方法塊是閉包,還有的地方說這種變量可以保存到作用域中的特性是閉包,不管咋樣反正閉包就是這么個事情。有一點要注意的是閉包可能會產生性能問題,因為父函數的“執行環境”對象沒有釋放會占據更多的內存空間,并且在進行標識符解析的過程中父函數的變量在嵌套函數中“執行環境”的第二層對象,所以在遍歷的時候會總是多遍歷當前嵌套函數的活動對象一次而產生性能的損耗。

  • 柯里化

柯里化是一種對閉包的應用,它的作用是把多參數函數轉換為一系列單參數函數并進行調用。一個最簡單極端的例子,實現一個求兩個數的和的函數,沒有柯里化的函數定義是function add(x+y){return x+y;},這個函數在調用的時候要一次性傳入兩個參數,add(1,2);而柯里化的寫法是function add(x){return function(y){return x+y}},這個函數在調用的時候要分次傳入一個參數,add(1)(2);這種寫法通用性很差,一個通用性比較好的柯里化方法應該是一個無參函數,并且要每次調用都要保存住傳入的參數并與原來傳入的參數進行合并,所以可以寫一個拓展方法Function.prototype.curry(){var  slice=Array.prototype.slice;var args=slice.call(arguments;var that=this;return function(){return that.apply(null,args.concat(slice.apply(arguments)));});}。好在EMCAScript5標準之后不用這么麻煩,5標準有一個bind方法,前面說過這個方法屬于函數的借用調用,bind不直接執行而是返回一個初始化了this的函數,其實bind不僅可以初始化this,還可以初始化其他的參數,所以bind方法第一個參數用來初始化this,可以為null,其后的參數都是初始化函數參數的,因此那個add函數可以這么調用add.bind(null,1)(3);

</div>

 本文由用戶 yunjian 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!