雖然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)函數被頻繁調用。
(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 下一篇文章 (四)設計模式