(三)閉包和高階函數

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

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

一:閉包java

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

1.變量做用域程序員

變量的做用域就是指變量的有效範圍,咱們最常談到的是在函數中聲明的變量做用域。web

當在函數中聲明一個變量時,若是沒有使用var關鍵字,這個變量就會變成全局變量(固然這是一種容易形成命名衝突的作法。)ajax

另一種狀況是用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 下一篇文章 (四)設計模式

相關文章
相關標籤/搜索