濃縮解讀《JavaScript設計模式與開發實踐》③

三、閉包和高階函數

IMG_20170112_004128.jpg

3.1 閉包

3.1.1 變量的作用域

  • 所謂變量的作用域,就是變量的有效范圍。通過作用域的劃分,JavaScript變量分為全局變量和局部變量。
  • 聲明在函數外的變量為全局變量;在函數內并且以var關鍵字聲明的變量為局部變量
  • 我們都知道,全局變量能在任何作用域訪問到,但這很容易造成命名沖突;而局部變量只有在函數里面能訪問到,這是因為JavaScript的查找變量的規則是從內往外搜索的。

3.1.2 變量的生命周期

  • 全局變量的生命周期是永久的(除非我們主動銷毀這個全局變量),而局部變量則當函數執行完畢時被銷毀。
  • 那JavaScript中是否存在,即便函數執行完畢,依然不會被銷毀的局部變量?答案是肯定的。
<script type="text/javascript">
    //現在有一個名為func的函數
    var func = function(){
        //①函數執行體中,將局部變量a賦值為1
        var a = 1;
        //②返回一個function執行環境
        return function(){
            //③執行環境中,將func.a局部變量加1,然后輸出到控制臺
            a++;
            console.info(a);
        }
    };
    
    //調用:將func函數執行后的返回,賦值給f
    var f = func();
    f();    //f()調用一次,輸出2
    f();    //f()再調用一次,輸出3
    f();    //f()接著調用,輸出4
</script>
  • 以上案例中的func.a局部變量,在func()函數執行過后并沒有被銷毀。每次執行f()時,仍能對它進行累加,就是佐證。這是因為func()返回了一個匿名函數的引用賦值給f,正是由于被外部變量引用了,所以不被銷毀,此時這個匿名函數就稱為閉包
  • 什么是閉包?

    在一個函數內定義另外一個函數(內部函數可以訪問外部函數的變量),如果將這個內部函數提供給其他變量引用時,內部函數作用域以及依賴的外部作用域的執行環境就不會被銷毀。此時這個內部函數就像一個可以訪問封閉數據包的執行環境,也就是閉包。

3.1.3 閉包的用途

  • 我們不但要學習什么是JavaScript閉包,更要了解如何利用閉包特性來寫代碼。由于篇幅有限,書中只羅列了幾個使用閉包的例子,但要知道實際開發中運用閉包非常廣泛,遠不止于此。
  1. 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
    var name = "William";
    return function(){
        console.info(name);          
    };
})();
person();   // 輸出成功
console.info(person.name);  //// 輸出失敗
  1. 延續變量的生命周期:我們經常用<img>標簽進行數據上報,創建一個臨時的img標簽,將需要上報的數據附加在img的url后綴,從而上送到服務器。如例子所示:
var report = function(dataSrc){
    var img = new Image();  //創建image對象
    img.src = dataSrc;  //將要上送的數據url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
  • 可經過排查發現,使用report()函數存在30%丟失數據的情況。這是因為,imgreport()函數中的局部變量,函數執行完畢后就被銷毀了,而這個時候往往HTTP請求還沒建立成功。而通過閉包來保存img變量可以解決請求丟失的問題:
//注意:我們將普通函數改成了自執行函數
var report = (function(){
    var imgs = [];
    return function(dataSrc){
        var img = new Image();
        images.push(img);
        img.src = dataSrc;
    }
})();
  1. 用閉包實現面向對象:我們經常使用過程數據來描述面向對象編程當中的對象。對象的方法包含了過程,而閉包則是在過程中以執行環境的方式包含了數據。
  • 既然閉包可以封裝私有變量,自然也能完成面向對象的設計。實際上,用面向對象思想能實現的功能,用閉包也能實現,反之亦然,這就是JavaScript的靈活之處。
  • 有這樣一段面向對象的JS代碼:
//Person構造器,里面有一個name屬性
var Person = function(){
  this.name = "William";
};
//給Person的原型添加一個sayName()方法
Person.prototype.sayName = function(){
    console.info("hello,my name is " + this.name);
};
//實例化Person
var person1 = new Person();
person1.sayName();
  • 用閉包可以實現同樣的效果:因為在JavaScript用new執行構造函數,本質也是返回一個對象
//person()函數返回一個有sayName()方法的對象
var person = function(){
    var name = "William";
    return {
        sayName : function(){
            console.info("hello,my name is " + name);
        }
    }
};
//執行person()函數,將返回的對象賦值給person1
var person1 = person();
//調用person1.sayName()方法
person1.sayName();
//控制臺輸出 "hello,my name is William"
  1. 用閉包實現命令模式
  • 命令模式是將請求封裝成對象,從而可以把不同的請求對象進行參數化、對請求對象排隊或者記錄日志以及執行可撤銷的操作。
  • 命令模式的能夠分離請求發起者和執行者之間的耦合關系。往往在命令被執行之前,就預先往命令對象中植入命令的執行者。
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <button id="execute">開啟</button>
        <button id="undo">關閉</button>
        <script type="text/javascript">
            var Tv = {
                open : function(){
                    console.info("打開電視機");
                },
                close : function(){
                    console.info("關閉電視機");
                }
            };
            var OpenTvCommand = function(receiver){
                this.receiver = receiver;
            };
            OpenTvCommand.prototype.execute = function(){
                this.receiver.open();
            };
            OpenTvCommand.prototype.undo = function(){
                this.receiver.close();
            };
            var setCommand = function(command){
                document.getElementById("execute").onclick = function(){
                    command.execute();
                }
                document.getElementById("undo").onclick = function(){
                    command.undo();
                }
            };
            //調用
            setCommand(new OpenTvCommand(Tv));
        </script>
    </body>
</html>
  • 用閉包實現命令模式:
<script type="text/javascript">
    var Tv = {
        open : function(){
            console.info("打開電視機");
        },
        close : function(){
            console.info("關閉電視機");
        }
    };
    var createCommand = function(receiver){
        var execute = function(){
            return receiver.open();
        }
        var undo = function(){
            return receiver.close();
        }
        return {
            execute : execute,
            undo : undo
        }
    };
    var setCommand = function(command){
        document.getElementById("execute").onclick = function(){
            command.execute();
        }
        document.getElementById("undo").onclick = function(){
            command.undo();
        }
    };
    //調用
    setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內存管理
  • 一直流傳著一種聳人聽聞的說法,聲稱閉包會造成內存泄漏,所以應當盡量避免使用閉包。
  • 局部變量本來應該在函數退出的時候被釋放,但在閉包形成的環境中,局部變量不被釋放。從這個意義上看,確實會造成一些數據無法被及時銷毀。但我們使用閉包,是我們主動選擇延長局部變量的生命周期,不能說成是內存泄漏。當使用完畢后,大可手動將這些變量設為null
  • 而只有閉包形成循環引用的情況下,才會導致內存泄漏。但這也不是閉包或者JavaScript的問題,我們可以避免循環引用的情況,而不是因噎廢食,徹底摒棄閉包。

3.2 高階函數

  • 高階函數是指滿足以下兩個條件之一的函數:
  1. 函數可以作為參數被傳遞;
  2. 函數可以作為返回值輸出;
  • 顯然,JavaScript語言中的函數兩個條件都滿足,下面將講解JavaScript高階函數特性的應用示例。。
3.2.1 函數作為參數傳入
  • 把函數當做參數傳遞,使得我們可以抽離出一部分容易變化的業務邏輯。
  • 這樣的例子在JavaScript代碼中比比皆是,比如jQuery中事件的綁定,或者jQuery中的ajax請求:
//按鈕監聽事件
$("btn").click(function(){
  console.info("btn clicked");
});
//可以發現,其本質就是執行了click()方法,然后傳入一個函數作為參數。
//注意到:在按鈕點擊后的處理是變化的,通過回調函數來封裝變化。
  • 另外還有Array.sort()。這是用來排序數組的一個方法,傳入一個自定義的函數來指定是遞增還是遞減排序。
var arr = [1,7,9,2];
//從小到大排序
arr.sort(function(){
  return a - b;
});
console.info(arr); //輸出 "[1, 2, 7, 9]"
//從大道小排序
arr.sort(function(){
  return b - a;
});
console.info(arr); //輸出 "[9, 7, 2, 1]"
3.2.2 函數作為返回值輸出
  • 讓函數返回一個可執行的函數,在之前的代碼我們已經接觸過了,這使得整個運算過程是可延續。
  • 我們通過優化一段類型判斷的JavaScript代碼來感受函數作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
    //通過傳入的obj對象執行toString()方法,將結果值和預期字符串比較
    return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數組
var isArray = function(obj){
    return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數字
var isNumber = function(obj){
    return Object.prototype.toString.call(obj) === '[object Number]';
}
  • 可以發現上面的代碼toString部分都是相同的,我們通過將函數作為返回值的方式優化代碼。
//抽象出一個類型判斷的通用函數
var isType = funcion(type){
    //該函數返回一個可執行的函數,用來執行toString方法和預期字符串做比較
    return function(obj){
        return Object.prototype.toString.call(obj) === '[Object '+type+']';
    }
}
//預先注冊具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調用
console.info(isArray([1,3,2]));  //輸出: true
  • 另外一個例子,是利用JavaScript函數作為返回值這個特性實現單例模式。
var getSingle = function(fn){
    var ret;  //臨時變量
    return function(){
        //如果ret已經存在的話則返回;否則新創建對象
        return ret || (ret = fn.apply(this,arguments));
    }
}
var getScript = getSingle(function(){
    return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
console.info(script1 === script2);//輸出: true
3.2.3 高階函數實現AOP
  • AOP(面向切面編程)是指將日志統計、安全控制、異常處理等與業務邏輯無關的模塊代碼獨立出來,通過“動態植入”的方式參入到業務邏輯模塊當中。這樣可以保持業務邏輯模塊的純凈和高內聚性。
  • Java語言可以通過反射和動態代理機制來實現AOP技術,而JavaScript函數作為返回值的特性就可以簡單的實現,這是JavaScript與生俱來的能力。
Function.prototype.invokeBefore = function(beforFn){
    var _self = this;   //原函數的引用
    return function(){
        //先執行傳入的before函數
        beforFn.apply(this,arguments);
        //然后再執行自身
        return _self.apply(this,arguments);
    }
}
Function.prototype.invokeAfter = function(afterFn){
    var _self = this;   //
    return function(){
        //先執行函數,并保存執行結果
        var ret = _self.apply(this,arguments);
        //然后再執行after函數
        afterFn.apply(this,arguments);
        //最后返回結果
        return ret;
    }
}
//定義一個方法,控制臺輸出2
var func = function(){
    console.info(2);
};
//指定func()函數執行前和執行后要做的事情
func = func.invokeBefore(function(){
    console.info(1);
}).invokeAfter(function(){
    console.info(3);
});
//調用func()函數,控制臺輸出 1 2 3
func();
3.2.4 高階函數實現柯里化
  • 函數柯里化(function currying)的概念是由注明數理邏輯學家Haskell Curry豐富和發展起來的,所以因此得名。
  • currying又稱為部分求值。currying函數首先接受一些參數,接受這些參數之后并不立即求值,而是返回另外一個函數,并將傳入的參數函數保存起來.等真正需要求值的時候,將之前傳入的所有參數一次性的求值.
  • 我們通過JavaScript,通過一個記賬的代碼來模擬currying函數
var currying = function(fn){
    var args = [];  //緩存對象
    return function(){
        if(arguments.length == 0){
            //如果傳入的參數為空,則直接返回結果
            return fn.apply(this,args);
        }else{
            //如果參數不為空,則將傳入參數push到args數組中緩存起來
            [].push.apply(args,arguments);
            //并返回函數本身
            return arguments.callee;
        }
    }
}
var cost = (function(){
    var money = 0;
    return function(){
        for(var i=0;l = arguments.length;i<l;i++){
            money += arguments[i];
        }
        return money;
    }
});
//轉換成currying函數
var cost = currying(cost);
cost(100);  //記賬100,未真正求值
cost(100);  //記賬100,未真正求值
cost(400);  //記賬400,未真正求值
console.info(cost());   //求值,并輸出:600
3.2.5 高階函數實現反柯里化
  • 通過call()apply()方法可以借用別的對象的方法,比如借用Array.prototype.push()方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個問題的.
//為Function對象的原型添加uncurrying方法
Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        var obj = Array.prototype.call(arguments);
        return self.apply(obj,arguments);
    }
}
//提取push方法并使用
var push = Array.prototype.uncurrying();
(function(){
    push(arguments,4);
    console.info(arguments);//輸出 [1,2,3,4]
})(1,2,3);
3.2.6 高階函數實現函數節流
  • JavaScript中的函數大多數都是由用戶主動觸發的,尤其在瀏覽器端的某些情況下函數被非常頻繁的調用,從而導致性能問題。
  • 比如用來監聽瀏覽器窗口大小的window.onresize事件,當瀏覽器窗口被不斷拉伸時,這個事件觸發的頻率會非常高;又比如元素的拖拽監聽事件onmousemove,如果元素被不停的拖拽,也會頻繁的觸發;還有最典型的監聽文件上傳進度的事件,由于需要不斷掃描文件用以在頁面中顯示掃描進度。導致通知的頻率非常之高,大約一秒鐘10次,遠超過人眼所能覺察的極限。
  • throttle函數就是解決此類問題的方案。throttle顧名思義節流器,借鑒的是工程學里的思想,比如用節流器來穩定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時間內函數的密集執行,達到穩定性能的作用。
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.info("resize come in");
},500);
3.2.7 高階函數實現分時函數
  • 函數節流是限制函數被頻繁調用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時也影響著頁面的性能。比如WebQQ加載好友列表,往往需要短時間內一次性創建成百上千個節點,嚴重影響頁面性能。
//模擬添加1000個數據
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = function(data){
    for (var i=0;l=data.length;i<l;i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
};
renderFriendList(ary);
  • 通過分時函數讓創建節點的工作分批進行。
//創建timeChunk函數
var timeChunk = function(ary,fn,count){
    var obj,t,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);
    }
};
//測試
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = timeChunk(ary,function(n){
    var div = document.createElement('div');
    div.innerHTML = i;
    document.body.appendChild(div);
},8);
renderFriendList(ary);
  • 除此之外,書中還有通過高階函數的特性實現惰性加載函數的案例,考慮到文章篇幅的關系,這里就不贅述了。

3.1 閉包

3.1.1 變量的作用域

  • 所謂變量的作用域,就是變量的有效范圍。通過作用域的劃分,JavaScript變量分為全局變量和局部變量。
  • 聲明在函數外的變量為全局變量;在函數內并且以var關鍵字聲明的變量為局部變量
  • 我們都知道,全局變量能在任何作用域訪問到,但這很容易造成命名沖突;而局部變量只有在函數里面能訪問到,這是因為JavaScript的查找變量的規則是從內往外搜索的。

3.1.2 變量的生命周期

  • 全局變量的生命周期是永久的(除非我們主動銷毀這個全局變量),而局部變量則當函數執行完畢時被銷毀。
  • 那JavaScript中是否存在,即便函數執行完畢,依然不會被銷毀的局部變量?答案是肯定的。
<script type="text/javascript">
    //現在有一個名為func的函數
    var func = function(){
        //①函數執行體中,將局部變量a賦值為1
        var a = 1;
        //②返回一個function執行環境
        return function(){
            //③執行環境中,將func.a局部變量加1,然后輸出到控制臺
            a++;
            console.info(a);
        }
    };
    
    //調用:將func函數執行后的返回,賦值給f
    var f = func();
    f();    //f()調用一次,輸出2
    f();    //f()再調用一次,輸出3
    f();    //f()接著調用,輸出4
</script>
  • 以上案例中的func.a局部變量,在func()函數執行過后并沒有被銷毀。每次執行f()時,仍能對它進行累加,就是佐證。這是因為func()返回了一個匿名函數的引用賦值給f,正是由于被外部變量引用了,所以不被銷毀,此時這個匿名函數就稱為閉包
  • 什么是閉包?

    在一個函數內定義另外一個函數(內部函數可以訪問外部函數的變量),如果將這個內部函數提供給其他變量引用時,內部函數作用域以及依賴的外部作用域的執行環境就不會被銷毀。此時這個內部函數就像一個可以訪問封閉數據包的執行環境,也就是閉包。

3.1.3 閉包的用途

  • 我們不但要學習什么是JavaScript閉包,更要了解如何利用閉包特性來寫代碼。由于篇幅有限,書中只羅列了幾個使用閉包的例子,但要知道實際開發中運用閉包非常廣泛,遠不止于此。
  1. 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
    var name = "William";
    return function(){
        console.info(name);          
    };
})();
person();   // 輸出成功
console.info(person.name);  //// 輸出失敗
  1. 延續變量的生命周期:我們經常用<img>標簽進行數據上報,創建一個臨時的img標簽,將需要上報的數據附加在img的url后綴,從而上送到服務器。如例子所示:
var report = function(dataSrc){
    var img = new Image();  //創建image對象
    img.src = dataSrc;  //將要上送的數據url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
  • 可經過排查發現,使用report()函數存在30%丟失數據的情況。這是因為,imgreport()函數中的局部變量,函數執行完畢后就被銷毀了,而這個時候往往HTTP請求還沒建立成功。而通過閉包來保存img變量可以解決請求丟失的問題:
//注意:我們將普通函數改成了自執行函數
var report = (function(){
    var imgs = [];
    return function(dataSrc){
        var img = new Image();
        images.push(img);
        img.src = dataSrc;
    }
})();
  1. 用閉包實現面向對象:我們經常使用過程數據來描述面向對象編程當中的對象。對象的方法包含了過程,而閉包則是在過程中以執行環境的方式包含了數據。
  • 既然閉包可以封裝私有變量,自然也能完成面向對象的設計。實際上,用面向對象思想能實現的功能,用閉包也能實現,反之亦然,這就是JavaScript的靈活之處。
  • 有這樣一段面向對象的JS代碼:
//Person構造器,里面有一個name屬性
var Person = function(){
  this.name = "William";
};
//給Person的原型添加一個sayName()方法
Person.prototype.sayName = function(){
    console.info("hello,my name is " + this.name);
};
//實例化Person
var person1 = new Person();
person1.sayName();
  • 用閉包可以實現同樣的效果:因為在JavaScript用new執行構造函數,本質也是返回一個對象
//person()函數返回一個有sayName()方法的對象
var person = function(){
    var name = "William";
    return {
        sayName : function(){
            console.info("hello,my name is " + name);
        }
    }
};
//執行person()函數,將返回的對象賦值給person1
var person1 = person();
//調用person1.sayName()方法
person1.sayName();
//控制臺輸出 "hello,my name is William"
  1. 用閉包實現命令模式
  • 命令模式是將請求封裝成對象,從而可以把不同的請求對象進行參數化、對請求對象排隊或者記錄日志以及執行可撤銷的操作。
  • 命令模式的能夠分離請求發起者和執行者之間的耦合關系。往往在命令被執行之前,就預先往命令對象中植入命令的執行者。
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <button id="execute">開啟</button>
        <button id="undo">關閉</button>
        <script type="text/javascript">
            var Tv = {
                open : function(){
                    console.info("打開電視機");
                },
                close : function(){
                    console.info("關閉電視機");
                }
            };
            var OpenTvCommand = function(receiver){
                this.receiver = receiver;
            };
            OpenTvCommand.prototype.execute = function(){
                this.receiver.open();
            };
            OpenTvCommand.prototype.undo = function(){
                this.receiver.close();
            };
            var setCommand = function(command){
                document.getElementById("execute").onclick = function(){
                    command.execute();
                }
                document.getElementById("undo").onclick = function(){
                    command.undo();
                }
            };
            //調用
            setCommand(new OpenTvCommand(Tv));
        </script>
    </body>
</html>
  • 用閉包實現命令模式:
<script type="text/javascript">
    var Tv = {
        open : function(){
            console.info("打開電視機");
        },
        close : function(){
            console.info("關閉電視機");
        }
    };
    var createCommand = function(receiver){
        var execute = function(){
            return receiver.open();
        }
        var undo = function(){
            return receiver.close();
        }
        return {
            execute : execute,
            undo : undo
        }
    };
    var setCommand = function(command){
        document.getElementById("execute").onclick = function(){
            command.execute();
        }
        document.getElementById("undo").onclick = function(){
            command.undo();
        }
    };
    //調用
    setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內存管理
  • 一直流傳著一種聳人聽聞的說法,聲稱閉包會造成內存泄漏,所以應當盡量避免使用閉包。
  • 局部變量本來應該在函數退出的時候被釋放,但在閉包形成的環境中,局部變量不被釋放。從這個意義上看,確實會造成一些數據無法被及時銷毀。但我們使用閉包,是我們主動選擇延長局部變量的生命周期,不能說成是內存泄漏。當使用完畢后,大可手動將這些變量設為null
  • 而只有閉包形成循環引用的情況下,才會導致內存泄漏。但這也不是閉包或者JavaScript的問題,我們可以避免循環引用的情況,而不是因噎廢食,徹底摒棄閉包。

3.2 高階函數

  • 高階函數是指滿足以下兩個條件之一的函數:
  1. 函數可以作為參數被傳遞;
  2. 函數可以作為返回值輸出;
  • 顯然,JavaScript語言中的函數兩個條件都滿足,下面將講解JavaScript高階函數特性的應用示例。。
3.2.1 函數作為參數傳入
  • 把函數當做參數傳遞,使得我們可以抽離出一部分容易變化的業務邏輯。
  • 這樣的例子在JavaScript代碼中比比皆是,比如jQuery中事件的綁定,或者jQuery中的ajax請求:
//按鈕監聽事件
$("btn").click(function(){
  console.info("btn clicked");
});
//可以發現,其本質就是執行了click()方法,然后傳入一個函數作為參數。
//注意到:在按鈕點擊后的處理是變化的,通過回調函數來封裝變化。
  • 另外還有Array.sort()。這是用來排序數組的一個方法,傳入一個自定義的函數來指定是遞增還是遞減排序。
var arr = [1,7,9,2];
//從小到大排序
arr.sort(function(){
  return a - b;
});
console.info(arr); //輸出 "[1, 2, 7, 9]"
//從大道小排序
arr.sort(function(){
  return b - a;
});
console.info(arr); //輸出 "[9, 7, 2, 1]"
3.2.2 函數作為返回值輸出
  • 讓函數返回一個可執行的函數,在之前的代碼我們已經接觸過了,這使得整個運算過程是可延續。
  • 我們通過優化一段類型判斷的JavaScript代碼來感受函數作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
    //通過傳入的obj對象執行toString()方法,將結果值和預期字符串比較
    return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數組
var isArray = function(obj){
    return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數字
var isNumber = function(obj){
    return Object.prototype.toString.call(obj) === '[object Number]';
}
  • 可以發現上面的代碼toString部分都是相同的,我們通過將函數作為返回值的方式優化代碼。
//抽象出一個類型判斷的通用函數
var isType = funcion(type){
    //該函數返回一個可執行的函數,用來執行toString方法和預期字符串做比較
    return function(obj){
        return Object.prototype.toString.call(obj) === '[Object '+type+']';
    }
}
//預先注冊具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調用
console.info(isArray([1,3,2]));  //輸出: true
  • 另外一個例子,是利用JavaScript函數作為返回值這個特性實現單例模式。
var getSingle = function(fn){
    var ret;  //臨時變量
    return function(){
        //如果ret已經存在的話則返回;否則新創建對象
        return ret || (ret = fn.apply(this,arguments));
    }
}
var getScript = getSingle(function(){
    return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
console.info(script1 === script2);//輸出: true
3.2.3 高階函數實現AOP
  • AOP(面向切面編程)是指將日志統計、安全控制、異常處理等與業務邏輯無關的模塊代碼獨立出來,通過“動態植入”的方式參入到業務邏輯模塊當中。這樣可以保持業務邏輯模塊的純凈和高內聚性。
  • Java語言可以通過反射和動態代理機制來實現AOP技術,而JavaScript函數作為返回值的特性就可以簡單的實現,這是JavaScript與生俱來的能力。
Function.prototype.invokeBefore = function(beforFn){
    var _self = this;   //原函數的引用
    return function(){
        //先執行傳入的before函數
        beforFn.apply(this,arguments);
        //然后再執行自身
        return _self.apply(this,arguments);
    }
}
Function.prototype.invokeAfter = function(afterFn){
    var _self = this;   //
    return function(){
        //先執行函數,并保存執行結果
        var ret = _self.apply(this,arguments);
        //然后再執行after函數
        afterFn.apply(this,arguments);
        //最后返回結果
        return ret;
    }
}
//定義一個方法,控制臺輸出2
var func = function(){
    console.info(2);
};
//指定func()函數執行前和執行后要做的事情
func = func.invokeBefore(function(){
    console.info(1);
}).invokeAfter(function(){
    console.info(3);
});
//調用func()函數,控制臺輸出 1 2 3
func();
3.2.4 高階函數實現柯里化
  • 函數柯里化(function currying)的概念是由注明數理邏輯學家Haskell Curry豐富和發展起來的,所以因此得名。
  • currying又稱為部分求值。currying函數首先接受一些參數,接受這些參數之后并不立即求值,而是返回另外一個函數,并將傳入的參數函數保存起來.等真正需要求值的時候,將之前傳入的所有參數一次性的求值.
  • 我們通過JavaScript,通過一個記賬的代碼來模擬currying函數
var currying = function(fn){
    var args = [];  //緩存對象
    return function(){
        if(arguments.length == 0){
            //如果傳入的參數為空,則直接返回結果
            return fn.apply(this,args);
        }else{
            //如果參數不為空,則將傳入參數push到args數組中緩存起來
            [].push.apply(args,arguments);
            //并返回函數本身
            return arguments.callee;
        }
    }
}
var cost = (function(){
    var money = 0;
    return function(){
        for(var i=0;l = arguments.length;i<l;i++){
            money += arguments[i];
        }
        return money;
    }
});
//轉換成currying函數
var cost = currying(cost);
cost(100);  //記賬100,未真正求值
cost(100);  //記賬100,未真正求值
cost(400);  //記賬400,未真正求值
console.info(cost());   //求值,并輸出:600
3.2.5 高階函數實現反柯里化
  • 通過call()apply()方法可以借用別的對象的方法,比如借用Array.prototype.push()方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個問題的.
//為Function對象的原型添加uncurrying方法
Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        var obj = Array.prototype.call(arguments);
        return self.apply(obj,arguments);
    }
}
//提取push方法并使用
var push = Array.prototype.uncurrying();
(function(){
    push(arguments,4);
    console.info(arguments);//輸出 [1,2,3,4]
})(1,2,3);
3.2.6 高階函數實現函數節流
  • JavaScript中的函數大多數都是由用戶主動觸發的,尤其在瀏覽器端的某些情況下函數被非常頻繁的調用,從而導致性能問題。
  • 比如用來監聽瀏覽器窗口大小的window.onresize事件,當瀏覽器窗口被不斷拉伸時,這個事件觸發的頻率會非常高;又比如元素的拖拽監聽事件onmousemove,如果元素被不停的拖拽,也會頻繁的觸發;還有最典型的監聽文件上傳進度的事件,由于需要不斷掃描文件用以在頁面中顯示掃描進度。導致通知的頻率非常之高,大約一秒鐘10次,遠超過人眼所能覺察的極限。
  • throttle函數就是解決此類問題的方案。throttle顧名思義節流器,借鑒的是工程學里的思想,比如用節流器來穩定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時間內函數的密集執行,達到穩定性能的作用。
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.info("resize come in");
},500);
3.2.7 高階函數實現分時函數
  • 函數節流是限制函數被頻繁調用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時也影響著頁面的性能。比如WebQQ加載好友列表,往往需要短時間內一次性創建成百上千個節點,嚴重影響頁面性能。
//模擬添加1000個數據
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = function(data){
    for (var i=0;l=data.length;i<l;i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
};
renderFriendList(ary);
  • 通過分時函數讓創建節點的工作分批進行。
//創建timeChunk函數
var timeChunk = function(ary,fn,count){
    var obj,t,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);
    }
};
//測試
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = timeChunk(ary,function(n){
    var div = document.createElement('div');
    div.innerHTML = i;
    document.body.appendChild(div);
},8);
renderFriendList(ary);
  • 除此之外,書中還有通過高階函數的特性實現惰性加載函數的案例,考慮到文章篇幅的關系,這里就不贅述了。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容