(三)閉包和高階函數


雖然javascript是一門面向對象的編程語言,但這門語言同時也同時擁有許多函數式語言的特性。

函數式語言的鼻祖是LISP,javascript設計之初參考了LISP兩大方言之一的Schenme,引入了Lambda表達式,閉包,高階函數等特性。使用這些特性,我們就可以靈活的編寫javascript代碼。

一:閉包

對於javascript程序員來說,閉包(closure)是一個難懂又必須征服的概念。閉包的形成與變量作用域以及變量的聲明周期密切相關。

1.變量作用域

變量的作用域就是指變量的有效范圍,我們最常談到的是在函數中聲明的變量作用域。

當在函數中聲明一個變量時,如果沒有使用var關鍵字,這個變量就會變成全局變量(當然這是一種容易造成命名沖突的做法。)

另外一種情況是用var關鍵字在函數中聲明變量,這時候的變量即局部變量,只有在函數內部才能訪問到這變量,在函數外面是訪問不到的,代碼如下:

var func = function() {
    var a = 1;
    console.log(a)
}
func()
console.log(a);//Uncaught ReferenceError: a is not defined

下面這段包含了嵌套函數的代碼,也許能幫助我們加深對遍歷搜索過程中的理解

var a = 1;
var func = function() {
    var b = 2;
    var func2 = function(){
        var c = 3;
        console.log(b);
        console.log(a)
    }
    func2()
    console.log(c) //Uncaught ReferenceError: c is not defined
}
func()

2.變量的生成周期

var func = function(){
    var a =1;
    console.log(a) //退出函數后局部變量a將銷毀
}
func()
var func2 = function(){
    var a = 2;
    return function() {
        a++;
        console.log(a)
    }
}
var f = func2();
f() //3
f() //4
f() //5
f() //6

func2根我們之前的推論相反,當退出函數后,局部變量a並沒有消失,而是停留在某個地方。這是因為,當執行 var f = func2()時,f返回了一個匿名函數的引用,它可以訪問到func()被調用時的所產生的環境,而局部變量a一直處在這個環境里。既然局部變量所在的環境還能被外界訪問,這個局部的變量就有了不被銷毀的理由。在這里產生了一個閉包環境,局部變量看起來被延續了。

利用閉包我們可以完成很多奇妙的工作,下面介紹一個閉包的經典應用。

假設頁面上有5個div節點,我們通過循環給div綁定onclick,按照索引順序,點擊第一個時彈出0,第二個輸出2,依次類推。

<div>div1</div>
<div>div2</div>
<div>div3</div>
<div>div4</div>
<div>div5</div>
<div>div6</div>

<script type="text/javascript">
var nodes = document.getElementsByTagName('div')
console.log(nodes.length)
for (var i = 0; i < nodes.length; i++) {
    nodes[i].onclick = function() {
        console.log(i)
    }
}
</script>

在這種情況下,發現無論點擊那個div都輸出6,這是因為div節點的onclick是被異步觸發的,當事件被觸發的時候,for循環早已經結束,此時的變量i已經是6。

解決的辦法是,在閉包的幫助下,把每次循環的i都封閉起來,當事件函數順着作用域鏈中從內到外查找變量i時,會先找到被封閉在閉包環境中的i,如果有6個div,這里的i就是0,1,2,3,4,5

var nodes = document.getElementsByTagName('div')
for (var i = 0; i < nodes.length; i++) {
    (function(i){
        nodes[i].onclick = function(){
            console.log(i+1)
        }
    })(i)
}

根據同樣的道理,我們還可以編寫如下一段代碼

var Type = {};

for (var i = 0 , type; type = ['String','Array','Number'][i++];){
    (function ( type ){
        Type['is' + type] = function( obj ) {
            return Object.prototype.toString.call( obj ) === '[object '+ type +']'
        }
    })( type )
}

console.log( Type.isArray([]) ) //true
console.log( Type.isString('') )//true

3.閉包的更多的作用

在實際開發中,閉包的運用十分廣泛

(1)封裝變量

閉包可以幫助把一些不需要暴露在全局的變量封裝成“私有變量”,假設一個計算乘積的簡單函數。

    var mult = function(){
        var a = 1;
        for (var i = 0, l = arguments.length; i < l; i++) {
            a = a * arguments[i]
        }
        return a
    }

    console.log(mult(10,2,4)) //80

mult函數每次都接受一些number類型的參數,並返回這些參數的乘積,現在我們覺得對於那些相同的參數來說,每次都進行一次計算是一種浪費,我們可以加入緩存機制來提高這個函數的性能。

var cache = {};

var mult = function(){
    var args = Array.prototype.join.call( arguments, ',' );
    if (cache[ args ]) {
        return cache[ args ]
    }

    var a = 1;
    for ( var i = 0, l = arguments.length; i<l;i++ ) {
        a = a * arguments[i]
    }

    return cache[ args ] = a;
}

console.log(mult(10,2,4)) //80

看到cache這個變量僅僅在mult函數中被使用,與其讓cache變量跟mult函數一起暴露在全局作用域下,不如將它封裝在mult內部,這樣可以減少頁面的全局變量,以避免在其它地方不小心修改而引發錯誤。

var mult = (function(){
    var cache = {};
    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if (args in cache){
            return cache[ args ]
        }

        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i]
        }

        return cache[ args ] = a;
    }
})()

console.log(mult(10,2,4,2)) //160

提煉函數是重構中一種常見的技巧。如果在一個大函數中有一些代碼能獨立出來,我們常常把這些小代碼塊封裝在獨立的小函數里面。獨立的小函數有助於代碼復用 ,如果這些小函數有一個良好的命名,它們本身起到了注釋的作用,這些小函數不需要在程序的其它地方使用,最好是他們用閉包封閉起來。代碼如下:

var mult = (function(){
    var cache = {};
    var calculate = function(){//封閉calculate函數
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i]
        }
        return a;
    }

    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if ( args in cache ){
            return cache[ args ];
        }
        return cache[ args ] = calculate.apply( null, arguments )
    }
})()
console.log(mult(10,2,4,2,2)) //320

(2)延續局部變量的壽命

img對象經常用於數據的上報,如下所示

var report = function( src ){
    var img = new Image()
    img.src = src;
}
report('http://.com/getUserinfo')

但是我們結果查詢后,得知,因為一些低版本瀏覽器的實現存在bug,在這些瀏覽器下使用report函數數據的上報會丟失30%,也就是說,reprot函數並不是每次都發起了請求。
丟失的原因是img是report函數中的局部變量,當report函數的調用結束后,img局部變量隨即被銷毀,而此時或許還沒有來的及發出http請求。所有此次的請求就會丟失掉。

現在我們將img變量用閉包封閉起來,便能解決請求丟失的問題。

var report = (function(){
    var img = [];
    return function( src ){
        var img = new Image();
        img.push( img );
        img.src = src;
    }
})()

4.閉包和面向對象設計

下面我們來看看跟閉包相關的代碼:

var extent = function(){
    var value = 0;
    return {
        call : function(){
            value++;
            console.log(value)
        }
    }
};
var bb = extent();

bb.call() //1
bb.call() //2
bb.call() //3

如果換成面向對象的寫法,就是:

var extent = {
    value : 0,
    call : function(){
        this.value++;
        console.log(this.value)
    }
}
extent.call();//1
extent.call();//2
extent.call();//3

或者,

var extent = function(){
    this.value = 0;
} 
extent.prototype.call = function(){
    this.value++;
    console.log(this.value)
}

var dd = new extent()
dd.call();//1
dd.call();//2
dd.call();//3

 二.高階函數

高階函數是至少滿足以下兩點的函數

  • 函數可以作為參數被傳遞
  • 函數可以作為返回值輸出

1)。函數作為參數傳遞

1.回調函數

在ajax的請求應用中,回調函數使用的特別頻繁,當我們想在ajax請求返回之后做一些事情。但又不知道確切的返回時間時,最常見的方案就是把callback函數作為參數傳入發起ajax請求的方法中,待請求完成時執行callback函數

var getUserInfo = function( userid, callback) {
    $.ajax('http://xxx.com/getUserInfo?' + userid, function( data ){
        if (typeof callback === 'function') {
            callback( data )
        }
    });
}
getUserInfo(1233,function( data ){
    console.log( data )
});

回調函數的應用不僅只在異步請求中,當一個函數不適合執行一些請求時,我們也可以把一些請求封裝成一個函數,並把它作為參數傳遞給另外一個函數,“委托”給另外一個函數來執行。

比如,我們想在頁面中創建100個DIV節點,然后把這些DIV節點都設置為隱藏。下面是一種編寫代碼的方式:

var appendDiv = function(){
    for (var i = 0; i < 100; i++){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild( div )
        div.style.display = 'none'
    }
}
appendDiv()

將 div.style.display = 'none' 的邏輯編碼在appendDiv里面是不合理的,appendDiv未免有點個性化,成為了一個難以復用的的函數,並不是每人創建了節點之后就希望它們立即隱藏。

於是我們將div.style.display = 'none'這行代碼抽出來,用回調函數傳入appendDiv方法:

var appendDiv = function( callback ){
    for (var i = 0; i < 100; i++){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild( div );
        if (typeof callback === 'function'){
            callback( div )
        }
    }
}
appendDiv( function( node ){
    node.style.display = 'none'
});

可以看到,隱藏節點的請求實際上是由客戶端發起的,但是客戶並不知道節點什么時候會創建好,於是把隱藏節點的邏輯放在回調函數中,“委托”給appendDiv方法。appendDiv方法方然知道節點什么時候創建好,所以在節點創建好的時候,appendDiv會執行之前客戶傳入的回調函數。

2.Array.prototype.sort

Array.prototype.sort接受一個函數當做參數,這個函數里面封裝了數組元素的排序規則。從Array.prototype.sort的使用可以看出,我們的目的是對數組進行排序,這是不變的部分;而使用什么規則去排序,則是可變的部分,把可變的部分封裝在函數參數里,動態傳入Array.prototype.sort,使Array.prototype.sort方法編程一個非常靈活的方法,代碼如下:

    //從小到大
    var cc = [1,4,3].sort(function( a, b ){
        return a - b;
    });
    console.log(cc);//[1, 3, 4]

    //從大到小
    var dd = [1,5,2,57,22].sort(function( a, b){
        return b - a;
    })
    console.log(dd) ;//[57, 22, 5, 2, 1]

2)。函數作為返回值輸出

相比把函數當做參數傳遞,函數當返回作返回值輸出的應用場景或更多,也能更體現函數式編程的巧妙。讓函數繼續返回一個可執行的函數,意味着運算過程是可延續的。

1.判斷數據的類型。

var isString = function ( obj ){
    return Object.prototype.toString.call( obj ) === '[object string]';
}

var isArray = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Array]';
}

var isMumber = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Number]'
}

我們發現,這些函數的大部分都是相同的,不同的只是Object.prototype.toString.call( obj )返回的字符串。,為了避免多余的代碼,我們嘗試把這些字符串作為參數提前值入isStype函數。代碼如下:

var isType = function( type ){
    return function( obj ){
        return Object.prototype.toString.call( obj ) === '[object ' + type +']';
    }
}

var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumer = isType( 'Number' );

console.log( isArray([1,2,3]) );

2.getSingle

下面是一個單例模式的例子,在后面的設計模式的學習中,我們將更深入的講解,這是暫且只理解其代碼的實現

var getSingle = function( fn ){
    var ret;
    return function(){
        return ret || ( ret = fn.apply( this, arguments) );
    }
};

3)高階函數實現AOP

AOP(面向切面編程)的主要作用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能通常包括日志的統計,安全控制,異常處理等。
這些功能抽離出來之后,再通過“動態織入”的方式摻入業務邏輯模塊中。這樣做的好處首先是可以保持業務邏輯模塊的純凈和高內聚性,其次是很方便的復用日志統計等功能模塊。

在java中,可以通過反射和動態代理機制來實現AOP技術。而在javascript這種動態語言中,AOP的實現更加簡單。這是javascript與生俱來的能力。

通常,在javascript中實現AOP,都是指把一個函數“動態織入”到另外一個函數中,具體的實現方式有很多,本書我們將通過擴展Function.prototype來做到這一點,代碼如下:

Function.prototype.before = function( beforefn ) {
    var _self = this;//保存對原函數的引用
    return function(){ //返回包含了原函數和新函數的“代理”的函數
        beforefn.apply( this, arguments ); //執行新函數,修正this
        return _self.apply( this, arguments );//執行原函數
    }
}

Function.prototype.after =  function( afterfn ) {
    var _self = this;
    return function(){
        var ret = _self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
}

var func = function() {
    console.log(2)
}

func = func.before(function(){
    console.log( 1 );
}).after(function(){
    console.log(3)
})

func()

我們把負責打印數字1和打印數字3的兩個函數通過AOP的方式動態植入func函數。通過執行,我們看到控制台返回1 2 3

三:函數節流


javascript中的函數大部分的情況是由用戶主動是觸發的,除非函數本身不合理,否則我們一般不會遇到跟性能相關的問題。但在一些少數的情況下,函數的觸發不是由用戶直接控制的。在這些場景下,函數有可能非常頻繁的被調用,從而造成大的性能問題,下面將舉例說明下這個問題。

(1)函數被頻繁調用。

  • window.onresize事件。我們給window綁定了resize事件,當瀏覽器的大小窗口被改變時,這個事件的觸發頻率非常高。如果我們在window.resize事件函數里有一些跟DOM相關的節點操作,往往是非常消耗性能的。這個時候瀏覽器有可能吃不消,或者卡頓。
  • mosemove事件。 同樣,如果我們給一個div綁定了拖拽事件(主要是mousemove),當div節點被拖動時,也會頻繁的觸發該拖拽事件。
  • 上傳進度。在一個文件被瀏覽器掃描並上傳文件之前,會對文件進行掃描並隨時通知javascript函數,以便在當前頁面中顯示當前正真的進度。但通知頻率非常高,大概一秒種10次,很顯然我們在頁面中沒有必要這么頻繁的去通知。

(2)函數節流的原理
我們通過整理上面提到的是哪個場景,發現它們共同面臨的問題是函數觸發的頻率太高。

(3)函數節流的實現。

關於函數節流的實現代碼有很多種,下面的throttle函數的原理是,將即將被執行的函數用setTimeout延遲一段時間執行。如果該執行延遲還沒有完成,則忽略接下來調用該函數的請求。throttle函數接受兩個參數,第一個參數需要被延遲執行的函數,第二個參數為延遲執行的時間。具體代碼如下:

var throttle = function( fn, interval ){
    var __self = fn, //保存需要被延遲執行的函數引用
    timer, //定時器
    firstTime = true; //是否第一次調用

    return function() {
        var args = arguments,
        __me = this;

        if( firstTime ) { //如果是第一次調用,不需要延遲執行
            __self.apply( __me, args );
            return firstTime = false;
        }
        if (timer) { //如果定時器還在,說明潛一次延遲執行還沒有完成
            return false;
        }

        timer = setTimeout(function(){//延遲一段時間執行
            clearTimeout(timer);
            timer = null;
            __self.apply(__me, args)
        }, interval || 500)
    } 
}

window.onresize = throttle(function(){
    console.log( 1 )
},500)

 (4)分時函數


在前面的函數節流的討論中,我們提供了一種限制函數被頻繁調用的解決方案,下面我們將遇到另外一個問題,某些函數確實是用戶主動調用的,但是因為一些客觀的原因,這些函數會嚴重的影響頁面的性能。

一個列子是webQQ創建QQ好友列表,列表中會有成千上萬的好友,如果一個節點一個節點的表示,當我們渲染這個列表時,可能一次要往一個頁面中創建成千上萬的節點。

在短時間內往頁面中添加大量DOM節點,會讓瀏覽器吃不消,造成假死。

var ary = [];

for (var i = 1; i <= 1000; i++){
    ary.push(i); //假設ary裝在了1000個好友
}

var renderList = function( data ){
    for (var i = 0, l = data.length; i < l; i++){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild( div )
    }
}

renderList( ary )

下面這個問題的解決方案之一是下面的timeChunk函數,timeChunk函數讓創建節點工作分批進行,比如1秒鍾創建1000個節點變為每隔200秒創建8個節點。

timeChunk函數接受3個參數,第一個參數是創建節點時需要用到的數據,第2個參數是封裝了創建節點邏輯的函數,第3個參數表示每一批創建節點的數量。
代碼如下:

var timeChunk = function( ary, fn, count ){
    var obj,
    t;
    var len = ary.length;
    var start = function(){
        for (var i = 0; i < Math.min( count || 1, ary.length ); i++) {
            var obj = ary.shift();
            fn( obj )
        }
    };

    return function(){
        t = setInterval(function(){
            if ( ary.length === 0 ){//如果全部節點以及都已經創建好
                return clearInterval(t)
            }
            start()
        },200)
    };
};

最后我們進行一些小測試,假設我們有1000個好友的數據,我們利用timeChunk函數,每一批只往頁面中創建8個節點:

var ary = [];

for (var i = 1; i <= 1000; i++){
    ary.push(i); //假設ary裝在了1000個好友
}

var renderList = timeChunk( ary, function( n ){
    var div = document.createElement('div');
    div.innerHTML = n;
    document.body.appendChild( div );
}, 8 );

renderList()

(5)惰性加載函數

在web開發中,因為瀏覽器的實現差異,一些嗅探工作總是不可避免。比如我們需要在一個各個瀏覽器中都能夠通用的事件綁定函數addEvent,常見寫法是如下:

var addEvent = function( elem, type, hander ){
    if ( window.addEventListener ){
        return elem.addEventListener( type, hander, false );
    }
    if (window.attachEvent) {
        return elem.attachEvent( 'on' + type, hander )
    }
};

這個函數的缺點是,當它每次被調用時都會執行里面的if條件分支,雖然執行這些if分支的開銷不算大,但也許有一些方法可以讓程序避免這些重復執行的過程。

第二種方案是這樣 ,我們把嗅探瀏覽器的操作提前到代碼加載的時候,在代碼加載的時候就進行一次判斷,以便讓addEvent返回一個包裹了正確的邏輯函數,代碼如下:

var addEvent = (function(){
    if ( window.addEventListener ){
        return function( elem, type, hander ){
            elem.addEventListener( type, hander, false );
        }
    }
    if ( window.attachEvent ){
        return function( elem, type, hander ){
            elem.attachEvent( 'on' + type, hander )
        }
    }
})();

目前addEvent函數依然有個缺點,也許我們從頭到尾都沒有使用過addEvent函數,這樣看來,前一次的瀏覽器嗅探就是完全多余的操作,而且這樣也會稍微延長頁面的ready時間。

第三種方法我們將要討論惰性載入函數方案。此時addEvent依然被聲明為一個普通函數,在函數里依然有一些分支判斷,但是在第一次進入條件分支后,在函數內部會重寫這個函數,重寫之后的函數就是我們期望的addEvent函數,在下一次幾個人addEvent函數的時候,addEvent函數里不再存在條件分支語句。

var addEvent = function( elem, type, handler ){
    if ( window.addEventListener ){
        addEvent = function( elem, type, handler ){
            elem.addEventListener( type, handler, false )
        }
    } else if ( window.attachEvent ){
        addEvent = function( elem, type, handler ){
            elem.attachEvent( 'on' + type, handler);
        }
    }
    addEvent( elem, type, handler )
}

var div = document.getElementById('div1');
addEvent( div, 'click', function(){
    alert('1')
});

addEvent( div, 'click', function(){
    alert('2')
})

 

本文完結

 

 上一篇文章: (二)this、call和apply 下一篇文章 (四)設計模式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM