JavaScript:回調模式(Callback Pattern) 函數就是對象,因此他們能夠做爲一個參數傳遞給其它函數; 當你將introduceBugs()做爲一個參數傳遞給writeCode(),而後在某個時間點,writeCode()有可能執行(調用)introduceBugs(); 這種狀況下,introduceBugs()被稱爲回調函數(callback function)或簡稱爲回調(callback:): function writeCode(callback) { // do something... callback(); // ... } function introduceBugs() { // ... make bugs } writeCode(introduceBugs); function writeCode(callback) { // do something... callback(); // ... } function introduceBugs() { // ... make bugs } writeCode(introduceBugs); 注意introduceBugs()做爲一參數傳遞給writeCode()是沒有使用括號的; 使用括號會當即執行函數,然而在這種狀況下,咱們但願的是隻傳遞一個指向函數的引用,讓writeCode()在適當的時候去執行; 一個回調的例子(A Callback Example) 咱們先從一個不使用回調的例子開始,而後在後面重構它; 假如,你有一個通用的函數,它會作一些複雜的工做而且返回一個包含不少數據的集合; 這個通用的函數可能被調用,而且它的工做就是去抓取一個頁面的DOM樹,返回一個數組裏麪包含着你感興趣的頁面元素的數組,好比findNodes(); var findNodes = function() { var i = 100000, // big, heavy loop nodes = [], // stores the result found; // the next node found while (i) { i -= 1; // complex logic here... nodes.push(found); } return nodes; }; var findNodes = function() { var i = 100000, // big, heavy loop nodes = [], // stores the result found; // the next node found while (i) { i -= 1; // complex logic here... nodes.push(found); } return nodes; }; 將這個函數保持通用性並讓它返回一個DOM節點(node)的數組是個好主意,但沒有對實際的元素作任何事情; 修改節點的邏輯可能在不一樣的函數中,好比一個叫hide()的函數,見名知意,它的做用是從頁面中隱藏節點: var hide = function(nodes) { var i = 0, max = nodes.length; for (; i < max; i += 1) { nodes[i].style.display = "none"; } }; // executing the functions hide(findNodes()); var hide = function(nodes) { var i = 0, max = nodes.length; for (; i < max; i += 1) { nodes[i].style.display = "none"; } }; // executing the functions hide(findNodes()); 這種實現是沒有效率的,由於hide()不得再也不遍歷一次findNodes()返回的的數組; 若是你能避免這個遍歷而且讓節點在findNodes()中一被選中就隱藏起來會更有效率; 可是如何你在findNodes()實現了隱藏的邏輯,那麼它將再也不是一個通用的函數,由於查詢和修改的邏輯產生了耦合; 加入回調模式——傳遞你隱藏節點的邏輯做爲一個回調函數而且代理它的執行: // refactored findNodes() to accept a callback var findNodes = function(callback) { var i = 100000, nodes = [], found; // check if callback is callable if (typeof callback !== "function") { callback = false; } while (i) { i -= 1; // complex logic here... // now callback: if (callback) { callback(found); } nodes.push(found); } return nodes; }; // refactored findNodes() to accept a callback var findNodes = function(callback) { var i = 100000, nodes = [], found; // check if callback is callable if (typeof callback !== "function") { callback = false; } while (i) { i -= 1; // complex logic here... // now callback: if (callback) { callback(found); } nodes.push(found); } return nodes; }; 這樣的實現是簡單明確的,惟一增長的工做就是findNodes()檢查了可選的回調函數是否有被提供,若是有,就執行它; 回調函數是可選的,因此重構後的findNodes()仍然能像之前同樣被使用,而且不會破壞依賴於舊的API的遺留代碼。 hide()函數的實現也能夠更加簡單,由於它不須要去遍歷節點數組: // a callback function var hide = function(node) { node.style.display = "none"; }; // find the nodes and hide them as you go findNodes(hide); // a callback function var hide = function(node) { node.style.display = "none"; }; // find the nodes and hide them as you go findNodes(hide); 回調函數能夠是一個在代碼中已經存在的函數,也能夠是一個匿名函數(當你調用主函數的時候纔會建立); 好比,怎樣使用相同的通用函數findNodes()去顯示節點: // passing an anonymous callback findNodes(function (node) { node.style.display = "block"; }); // passing an anonymous callback findNodes(function (node) { node.style.display = "block"; }); 回調和做用域(Callbacks and Scope) 在前面這個例子中,回調函數執行的部分可能像: callback(parameters); callback(parameters); 雖然這樣很簡單而且在不少狀況下都已經足夠了; 但常常有一些場景,回調函數不是匿名函數或者全局函數,而是一個對象的一個方法; 若是回調函數使用this去訪問函數屬於的對象,這就會產生意想不到的錯誤。 假若有一個parint()的回調函數,它是myapp對象的一個方法: var myapp = {}; myapp.color = "green"; myapp.paint = function(node) { node.style.color = this.color; }; var myapp = {}; myapp.color = "green"; myapp.paint = function(node) { node.style.color = this.color; }; findNodes()函數作了相似下面的事: var findNodes = function(callback) { // ... if (typeof callback === "function") { callback(found); } // ... }; var findNodes = function(callback) { // ... if (typeof callback === "function") { callback(found); } // ... }; 若是你調用了findNodes(myapp.paint),它並不能按照預期的那樣工做,由於this.color將會是undefined; 這裏this將會指向全局對象,由於findNodes()是一個全局函數; 若是findNodes()是一個叫作dom對象的方法,那麼在回調函數中的this將會指向dom而不是指望的myapp; 解決這個問題的方法就是傳遞一個回調函數,此外再傳遞這個回調函數屬於的對象做爲一個參數: findNodes(myapp.paint, myapp); findNodes(myapp.paint, myapp); 緊跟着,咱們須要去修改findNodes()去綁定(bind)傳遞進來的對象: var findNodes = function(callback, callback_obj) { //... if (typeof callback === "function") { callback.call(callback_obj, found); } // ... }; var findNodes = function(callback, callback_obj) { //... if (typeof callback === "function") { callback.call(callback_obj, found); } // ... }; 對於傳遞一個對象和一個被用來回調的方法,另外一個可選的方法就是將方法做爲字符串傳遞,那麼你就不會重複對象兩次; 換言之: findNodes(myapp.paint, myapp); findNodes(myapp.paint, myapp); 會變成: findNodes("paint", myapp); findNodes("paint", myapp); 那麼findNodes()可能會作一些事,就像下面幾行: var findNodes = function(callback, callback_obj) { if (typeof callback === "string") { callback = callback_obj[callback]; } //... if (typeof callback === "function") { callback.call(callback_obj, found); } // ... }; var findNodes = function(callback, callback_obj) { if (typeof callback === "string") { callback = callback_obj[callback]; } //... if (typeof callback === "function") { callback.call(callback_obj, found); } // ... }; 匿名的事件監聽器(Asynchronous Event Listeners) 回調模式在平常中被常用,好比,當你附加一個事件監聽器給頁面上的某個元素時,你實際上提供了一個指向了回調函數的引用,而且在事件發生時被調用; 這裏有個例子,怎麼將console.log()做爲一個回調函數監聽文檔的click事件: document.addEventListener("click", console.log, false); document.addEventListener("click", console.log, false); 絕大部分客戶端瀏覽器都是事件驅動的(event-driven); 當一個頁面加載完成,會觸發load事件,而後用戶能夠經過和頁面交互觸發各類各樣的事件,好比:click, keypress, mouseover, mousemove等等; 由於回調模式,JavaScript特別適合事件驅動編程,能讓你的程序異步的工做,換言之,就是不受順序限制。 「Don’t call us, we’ll call you」 在好萊塢中是句名言,在好萊塢對於一部電影中的一個角色每每有很候選人,劇組人員不可能一直答覆全部候選人打來的電話; 在異步的事件驅動的JavaScript,有個類似的情景,你提供一個回調函數用於在正確的時候被調用(to be called),而不是電話號碼; 你甚至可能提供比實際請求還要多的回調函數,由於某些事件可能不會發生; 好比:若是用戶不點擊「購買」按鈕,那麼你用於驗證表單格式的函數永遠不會被調用。 Timeouts 另外一個使用回調模式的例子就是使用瀏覽器的window對象的setTimeout()和setInterval()方法,這些方法也能夠接受和執行回調函數: var thePlotThickens = function () { console.log('500ms later...'); }; setTimeout(thePlotThickens, 500); var thePlotThickens = function () { console.log('500ms later...'); }; setTimeout(thePlotThickens, 500); 再次注意一下,thePlotThickens是如何被做爲一個參數傳遞的,沒有使用括號; 由於你不想它當即執行;傳遞字符串"thePlotThickens()"取代函數的引用和eval()相似,是很差的模式。 類庫中的回調(Callbacks in Libraries) 回調是一種簡單而強大的模式,當你在設計類庫的時候會派的上用場; 在軟件類庫中的代碼應該儘量的通用和複用,回調能夠幫助咱們解決這種泛化; 你不須要預測和實現你能夠想到的全部功能,由於它們會使類庫膨脹,而且大部分用戶都不會須要這麼多功能; 取而代之的是,集中精力在覈心的功能並提供以回調函數形式的「鉤子」(hook),這會讓類庫的方法更加簡單的去構建,擴展和定製。