深刻解析js中的函數

寫在前面

因爲詞語匱乏,本文繼續沿用"深刻解析xxx"這個俗套的命名,可是是真的很深刻(你要信我啊)。若是本文對你有用,歡迎收藏,若是喜歡個人文章,歡迎點贊和關注專欄。
函數能夠說是js的基礎,無處不在,功能又十分強大,本文將簡單介紹函數的特色而且重點介紹各類各樣的用法。廢話很少說,開車~
友情提示,因爲本文涵蓋的內容比較全面,難免篇幅稍長,中途請注意休息。css

函數簡介

可是其實,函數的本質就是對象。確切一點來講,實際上是第一類對象(first-class object)。關於第一類對象,wiki解釋以下:java

第一類對象又稱第一類公民,在編程語言中指的是一個具備如下特性的實體:node

  1. 可以做爲參數被傳遞
  2. 可以從一個函數結果中返回
  3. 可以被修改和賦值給變量

雖然看起來高大上,可是咱們只要先記住,在js裏函數也是對象,能夠擁有本身的屬性和方法,而它和通常js對象的區別是:能夠被調用,也就是可執行web

固然,函數還有一個明顯的特色就是,提供做用域:在函數做用域內的變量都是局部變量,對外部不可見。因爲js中其餘代碼塊,好比forwhile循環等並不提供做用域,因此有不少地方會利用函數來控制做用域。在後面會一一提到。編程

預備知識

這一塊在以前講閉包的時候其實提到了一些,可是仍是簡單介紹下。segmentfault

函數做用域

在相似C語言的編程語言中,花括號{}表示一個做用域:在做用域內的變量對外不可見,這個稱爲塊級做用域,可是在js中沒有塊級做用域,只有函數做用域:在函數體內聲明的變量,在整個函數體內有定義api

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的時候不報錯,運行的時候才報錯。看不懂請再回顧下預備知識的聲明提高部分。

函數參數

函數的參數通常分紅形參和實參,形參是函數定義時預期傳入的參數,實參是函數調用時實際傳入參數。

參數數量不對等狀況和arguments

Javascript沒有在函數調用時對實參作任何檢查。 因此可能出現如下狀況:

  • 當傳入的實參比形參個數要少的時候,剩下的形參會被自動設置爲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並非數組,而是一個對象,只是剛好使用數字爲索引

calleecaller

es5的非嚴格模式下,咱們可使用calleecaller這兩個屬性,

  • 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<nodes.length;i++){
        //根據條件篩選
        if(...){
            result.push()
        }
    }
    return result
}

//操做函數
function operte(nodes){
    var  i = 0;
    for(i = 0; i<node.length;i++){
        // 樣式操做
        node[i].style...
    }
}

按照上面定義的2個函數,先用filterNodes篩選符合要求額節點,而後將結果做爲operate函數的參數,這樣邏輯上是徹底沒問題的,只是有一個地方:其實咱們已經2次遍歷了符合要求的節點:第一次是在篩選時,第二次是在樣式操做時。這裏有辦法優化嗎?,若是咱們直接把樣式操做直接寫到result.push()後面,是能夠減小一次遍歷的,可是這樣filterNodes函數就不是一個純粹的篩選節點的數了。因此咱們可使用回調模式來解決,只需稍微修改下:

//篩選函數
function filterNodes(nodes,callback){
    var  i = 0;
    var result = [];
    for(i = 0; i<nodes.length;i++){
        //根據條件篩選
        if(...){
            result.push()
            
            //在這裏判斷是否傳遞了樣式操做函數,若是有,就執行樣式操做
            if(callback){
                callback(nodes[i])
            }
        }
    }
    return result
}

function operte(node){
    //這裏就沒必要再次循環了
    // 樣式操做
    node[i].style...
}

這樣改造以後,2個函數依然各自擁有本身的邏輯,並且咱們能夠經過調用filterNodes時,傳遞不一樣參數的辦法,來控制咱們想要的功能。

回調函數還有不少的常見用途:

  1. 異步事件監聽
    最多見的例子莫過於咱們爲文檔添加監聽事件:

    document.addEventListener("click",[回調函數],false)

    有了回調模式之後,程序能夠以異步的模式運行:只有用戶觸發了某些交互行爲,纔會調用到咱們指定的函數。

  2. 超時方法 setTimeout()setTimeInterval()
    這兩個函數也同樣接受回調函數

    setTimeout([回調函數],200)
  3. 軟件庫設計
    設計一個庫的時候,很重要的就是設計通用性和複用性的代碼,由於沒法提早預測到須要的每個功能,並且用戶也不會老是須要用到全部的功能,利用回調模式,很容易設計出具備核心功能有同時提供自選項的函數(好比前面提到的節點篩選函數,核心功能是篩選,又能根據須要插入後續操做)。

返回函數

剛剛在回調函數部分,說的是函數做爲另外一個函數的參數傳遞,接下來講說函數做爲另外一邊函數的結果返回。看下面一個計時器例子:

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篇,哈哈哈)但願今年本身能夠好好努力,把「深刻」系列貫徹到底。也但願你們都有所進步。而後依然是每次都同樣的結尾,若是內容有錯誤的地方歡迎指出;若是對你有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處,若是有問題也歡迎私信交流,主頁添加了郵箱地址~溜了

相關文章
相關標籤/搜索