精讀JavaScript模式(五),函數的回調、閉包與重寫模式
一、前言
今天地鐵上,看到很多拖著行李箱的路人,想回家了。
在上篇博客結尾,記錄到了函數的幾種創建方式,簡單說了下創建差異,以及不同瀏覽器對於name屬性的支持,這篇博客將從第四章函數的回調模式說起。我想了想,還是把一篇博客的知識點控制在五個以內,太長了我自己都懶得看,而且顯得特別混雜。標題還是簡要說下介紹了哪些知識,也方便自己以後查閱,那麽開始。
二、函數的回調模式
1.什麽是函數回調模式?
當調用函數時,我們可以將函數作為參數傳入到需要調用的函數中,例如我們為函數A傳入一個函數B,當函數A執行時調用了函數B,那麽我們可以說函數B是一個回調函數,簡稱回調。
functionA(data){ data(); }; function B(){ console.log(1); }; A(B);//將函數B作為參數傳入到A函數中進行調用
當B函數作為參數傳入A函數時,此時的B函數是不帶括號的,因為函數名帶括號時表示立即執行,這點大家應該都知道。
回調函數可以是一個匿名函數,其實這種寫法在編程中反而更為常見。
function A(data){ data(); }; A(function (){ console.log(2); });//2
2.回調函數作為對象的方法時this指向問題
回調函數通常的寫法是callback(parameters)
//回調函數sayName是obj的一個方法 let obj = { name : ‘echo‘, sayName : function () { console.log(this.name); } }; let func = function (callback) { callback() }; func(obj.sayName);//並不會輸出echo
我們原本預期是輸出echo,但實際執行時this指向了全局window而非obj,所以不能拿到name屬性,如何解決呢,在傳遞回調函數時,我們也可以把回調函數所屬對象也作為參數傳進去。調用方式改為如何即可,通過call或者apply改變this指向。
//回調函數sayName是obj的一個方法 let obj = { name : ‘echo‘, sayName : function () { console.log(this.name); } }; let func = function (callback, obj) { callback.call(obj) }; func(obj.sayName, obj);//echo
在上述代碼中,回調函數作為參數的寫法是obj.sayName,其實也可以直接傳一個字符串sayName進去,通過obj[sayName]執行,這樣做的好處是,函數執行時this直接指向了調用的obj,所以就不需要call額外修改this指向了。
//回調函數sayName是obj的一個方法 let obj = { name : ‘echo‘, sayName : function () { console.log(this.name); } }; let func = function (callback, obj) { obj[callback](); }; func(‘sayName‘, obj);//echo
3.回調模式的使用價值
在瀏覽器中大部分編程都是事件驅動的,例如頁面加載完成觸發load事件,用戶點擊觸發click事件,也真是因為回調模式的靈活性,才讓JS事件驅動編程如此靈活。回調模式能讓程序‘異步‘執行,也就是不按代碼順序執行。
我們可以在程序中定義多個回調函數,但它們並不是在代碼加載就會執行,等到時間成熟,例如用戶點擊了某個元素,回調函數才會根據開始執行。
document.addEventListener("click", console.log, false);
除了事件驅動,另一個常用回調函數的情景就是結合定時器,setTimeout()與setInterval(),這兩個方法的參數都是回調模式。
let func = function () { console.log(1); }; setTimeout(func, 500);//500ms後執行一次func函數 setInterval(func, 500)//每隔500ms執行一次func函數
再次強調的是,定時器中回調函數的寫法是func,並未帶括號,如果帶了括號就是立即執行。另一種寫法是setTimeout(‘func()‘, 500),但這是一種反模式,並不推薦。
三、返回函數作為返回值
函數是對象,除了可以作為參數同樣也能作為返回值,也就是說函數的返回值也能是一個函數。
function demo () { console.log(1); return function () { console.log(2); }; }; let func1 = demo();//1 func1()//2
在上述代碼中函數demo將返回的函數包裹了起來,創建了一個閉包,我們可以利用閉包存儲一些私有屬性,而私有屬性可以通過返回的函數操作,且外部函數不能直接讀取這些私有屬性。
const setup = function () { let count = 0; return function () { return (count += 1); }; }; let next = setup(); next();//1 next();//2 next();//3
四、函數重寫
當我們希望一個函數做一些初始化操作,並且初始化的操作只執行一次時,面對這種情況我們就需要使用函數重寫。
let handsomeMan = function () { console.log(‘echo is handsome man!‘); handsomeMan = function () { console.log(‘Yes,echo is handsome man!‘); }; }; handsomeMan()//echo is handsome man! handsomeMan()//Yes,echo is handsome man!
上方代碼中,第一次調用的輸出只會執行一次,這是因為新的函數覆蓋掉了舊函數,雖然一直都是調用handsomeMan,但前後執行函數完全不同。
這種模式的另一名字是函數的懶惰定義,因為函數是執行一次後才重新定義,相比分開兩個函數來寫,這樣的執行效率更為高效。
此模式有個明顯的問題就是,一旦重新函數被重寫,最初函數的所有方法屬性都將丟失。
let handsomeMan = function () { console.log(‘echo is handsome man!‘); handsomeMan = function () { console.log(‘Yes,echo is handsome man!‘); }; }; handsomeMan.property = "properly"; let boy = handsomeMan; boy();//echo is handsome man! boy();//echo is handsome man! console.log(boy.property);//properly //property屬性已丟失 handsomeMan()//echo is handsome man! handsomeMan()//Yes,echo is handsome man! console.log(handsomeMan.property);//undefined
五、立即執行函數
所謂立即執行函數就是一個在創建後就會被立即執行的函數表達式,也可以叫自調用函數。
//調用括號在裏面 (function () { console.log(1); }()); //調用括號在外面 (function () { console.log(2); })();
它主要由三部分組成,一對括號(),裏面包裹著一個函數表達式,以及一個自調的括號(),這個括號可緊跟函數,也可以寫在外面。我個人常用第二種寫法,但JSLint更傾向於第一種寫法。
立即執行函數長用於處理代碼初始化的工作,因為它提供了一個獨立的作用域,所有初始化中存在的的變量都不會汙染到全局環境。
立即執行函數也可以傳遞參數,像這樣
(function (data) { console.log(data); })(1);
除了傳參,立即執行函數也可以返回值,並且這些返回值可以賦值給變量。
var result = (function () { return 2 + 2; })(); console.log(result);//4
在對應一個對象的屬性時,也可以使用立即執行函數,假設對象的某個屬性是一個待確定的值,那我們就可以使用此模式來初始化該值。
let o = { message: (function () { return 2+2 })(), getMsg: function () { return this.message; } }; o.message;//4 o.getMsg()//4
需要註意的是,此時的o.message是一個字符串,並非一個函數。
六、代碼初始化的意義
在函數重寫和自調用函數模式中多次提到了代碼初始化,為什麽要做代碼初始化,簡單舉例說下。
JS的函數監聽大家都不會陌生,而早期IE與大部分瀏覽器提供的監聽綁定方法不同,如果不使用初始化,可能是這樣
let o = { addListener : function (el, type ,fn) { if(typeof window.addEventListener === ‘function‘) { el.addEventListener(type, fn, false); }else if (typeof document.attachEvent === ‘function‘) { el.attachEvent(‘on‘ + type, fn); }else{ el[‘on‘ + type] = fn; } } }; o.addListener();
當我們調用o.addListener()方法時,很明顯效率不高,每次調用都要把各瀏覽器判斷走一遍,才能確定最終的監聽綁定方式;我們初始化監聽方式。
let o = { addListener: null }; if (typeof window.addEventListener === ‘function‘) { o.addListener = function (el, type, fn) { el.addEventListener(type, fn, false); }; }else if (typeof document.attachEvent === ‘function‘) { o.addListener = function (el, type, fn){ el.attachEvent(‘on‘ + type, fn); } }else{ o.addListener = function () { el[‘on‘ + type] = fn; } };
在當我們調用o.addListener()方法時,此時addListener已經初始化過了,不用反反復復走監聽綁定判斷,這就是代碼初始化的意義,把那些你能確定下來,但需要繁瑣執行的邏輯一次性確定好,之後就是直接使用的操作了,就是這麽個意思。
這篇就記錄這麽多吧,還有五天回家過年了!
精讀JavaScript模式(五),函數的回調、閉包與重寫模式