雖然javascript是一門面向對象的編程語言,但這門語言同時也同時擁有許多函數式語言的特性。函數式語言的鼻祖是LISP,javascript設計之初參考了LISP兩大方言之一的Schenme,引入了Lambda表達式,閉包,高階函數等特性。使用這些特性,我們就可以靈活的編寫javascript代...
雖然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
(此文尚未完結,請關註更新)
上一篇文章: (二)this、call和apply