1. 程式人生 > >精讀JavaScript模式(五),函數的回調、閉包與重寫模式

精讀JavaScript模式(五),函數的回調、閉包與重寫模式

返回 早期 如果 str 表示 info 圖片 mem c函數

一、前言

今天地鐵上,看到很多拖著行李箱的路人,想回家了。

在上篇博客結尾,記錄到了函數的幾種創建方式,簡單說了下創建差異,以及不同瀏覽器對於name屬性的支持,這篇博客將從第四章函數的回調模式說起。我想了想,還是把一篇博客的知識點控制在五個以內,太長了我自己都懶得看,而且顯得特別混雜。標題還是簡要說下介紹了哪些知識,也方便自己以後查閱,那麽開始。

技術分享圖片

二、函數的回調模式

1.什麽是函數回調模式?

當調用函數時,我們可以將函數作為參數傳入到需要調用的函數中,例如我們為函數A傳入一個函數B,當函數A執行時調用了函數B,那麽我們可以說函數B是一個回調函數,簡稱回調。

function
A(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)

,通常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模式(五),函數的回調、閉包與重寫模式