这篇,我们仍旧继续学习函数。
二、回调模式
函数都是对象,这表示它们可以作为参数传递给其它函数。
function writeCode(callback) { // 执行一些事务... callback(); // ... } function introduceBugs() { // ...引入漏洞 } writeCode(introduceBugs);
请注意introduceBugs()作为参数传递给writeCode()时是不带括号的。括号表示要执行函数,而在这种情况下,我们仅需要传递该函数的应用,而让writeCode()在适当的时候来执行它(也就是说,返回以后调用)。
回调示例
我们来看个例子,假设我们需要一个通用函数执行一些复杂逻辑后返回一个大块数据的结果。
var findNodes = function() { var i = 100000,// 大而繁重的循环 nodes = [],// 存储结果 found;// 找到了下一个节点 while(i) { i -= 1; // 这里是复杂的逻辑... nodes.push(found); } return nodes; }
保持函数的通用性并且使其返回一个DOM节点数组,而不对实际元素做任何处理,这是一个非常好的思想。修改节点的逻辑,可以在其他函数中实现。
var hide = function (nodes){ var i = 0,max = nodes.length; for(;i < max;i += 1) { nodes[i].style.display = "none"; } }; //执行该函数 hide(findNodes());
上面的实现方式是低效的。因为hide()必须再次遍历由findNodes()返回的数组节点。如果能避免这种循环,并且只要在findNodes()中便可隐藏节点,那么这将是高效的实现方式。但是如果在findNodes()中实现隐藏逻辑,由于检索和修改逻辑耦合,那么它不再是一个通用函数。对这种问题的解决方法是采用回调模式,可以将节点隐藏逻辑以回调函数方式传递给findNodes()并委托其执行:
// 重构findNodes()以接受一个回调函数 var findNodes = function(callback) { var i = 100000,// 大而繁重的循环 nodes = [],// 存储结果 found;// 找到了下一个节点 // 检查回调函数是否为可调用的 if(typeof callback !== "function") { callback = false; } while(i) { i -= 1; // 这里是复杂的逻辑... // 现在运行回调函数 if(callback){ callback(found); } nodes.push(found); } return nodes; }
这是一种很直接的实现方法。findNodes()执行的唯一额外任务是,检查是否提供了可选回调函数,如果存在就执行。其中,回调函数是可选的,所以重构后的findNodes()仍然可以像以前一样使用。
现在,hide()的实现要简单很多,因为他不需要遍历所有的节点:
var hide = function (node){ node.style.display = "none"; }; //执行该函数 findNodes(hide);
如上所示,回调函数可以是一个已有的函数,也可以是一个匿名函数,可以在调用主函数时创建它。例如,下面的代码展示了如何使用同样的findNodes()函数以显示节点:
//传递一个匿名函数 findNodes(function(node) { node.style.display = "block"; });
回调与作用域
在前面的例子中,回调执行的语句部分如下:
callback(parameters);
虽然在大多数情况下,这种方法都是简单而有效的,但经常存在一些场景,其回调并不是一次性的匿名函数或全局函数,而是对象的方法。如果该回调方法使用this来引用他所属的对象,这可能会导致意想不到的行为发生。
// 重构findNodes()以接受一个回调函数 var findNodes = function(callback) { var i = 100000,// 大而繁重的循环 nodes = [],// 存储结果 found;// 找到了下一个节点 // 检查回调函数是否为可调用的 if(typeof callback !== "function") { callback = false; } while(i) { i -= 1; // 这里是复杂的逻辑... // 现在运行回调函数 if(callback){ callback(found); } nodes.push(found); } return nodes; } // 上面是我复制的 var myapp = {}; myapp.color = "green"; myapp.paint = function(node) { node.style.color = this.color; }; findNodes(myapp.paint)
如果你真的这样做了,那么他肯定不会按照预期那样执行。这是因为this.color没有被定义。因为findNodes()是一个全局函数,所以,this指向了全局对象。类似的,如果findNodes()是一个名为dom的对象的方法(dom.findNodes()),那么回调内部的this将指向dom,而不是预期的myapp。
对这个问题的解决方案是传递回调函数,并且另外还传递该回调函数所属的对象:
findNodes(myapp.paint,myapp);
然后,当然还需要修改findNodes()以绑定所传递进入的对象:
var findNodes = function(callback,callback_obj) { // ... if(typeof callback === 'function') { callback.call(callback_obj,found); } // ... };
我们发现应用了call(),后面会详细的讲解call()和apply()的相关内容。
上面的方法,可以稍加优化,无需两次输入该对象的名称:
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); } // ... };
异步事件监听
回调模式有许多常见用途,比如,当附加一个事件监听器到页面上的一个元素时,实际上提供了一个回调函数指针,该函数将会在时间发生时被调用。下面是一个简单的例子,展示了当监听到文档点击事件时如何传递回调函数console.log()。
document.addEventListener("click", console.log, false);
大多数的客户端浏览器编程都是事件驱动的。比如页面加载完成会触发load事件、用户与页面交互时,将会触发比如click、keypress、mouseover、mousemove等。JavaScript特别适合于事件驱动编程,因为回调模式支持程序以异步方式运行,也就是说,可以乱序方式运行。
超时
使用回调模式的另一个例子是,当使用浏览器的window对象所提供的超时方法:setTimeout()和setInterval()。这些方法也会接受并执行回调函数:
var thePlotThickens = function() { console.log("500ms later..."); }; setTimeout(thePlotThickens,500);
再次强调,这里函数thePlotThickens是如何以变量方式传递的,传递该函数时并没有带括号,因为并不想立即执行该函数,而只是想指向该函数以便setTimeout()在以后使用。
库中的回调模式
回调模式是一种简单而又强大的模式,当设计一个库时他可以派上用场。进入软件库的代码应该尽可能地是通用和可服用的代码。而回调可以帮助实现这种通用化。不需要预测和实现能想到的每一项功能,因为这样会迅速使库膨胀,而绝大多数用户永远不会需要其中大量的功能。相反,可以专注于核心功能并提供“挂钩”形式的回调函数,这将使您很容易的构建、扩展,以及自定义库方法。
三、返回函数
函数也是对象,因此它们也可以被用做返回值。这表示一个函数并不需要以某种数据值或数据数组作为执行结果返回。函数可以返回另一个更专门的函数,也可以按需创建另一个函数,这取决于其输入。
var setup = function () { alert(1); return function() { alert(2); }; }; // 使用setup函数 var my = setup(); // alerts 1 my(); // alerts 2 // 由于setup()包装了返回函数,它创建了一个闭包,可以使用这个闭包存储一些私有数据, // 而这些数据仅可被该返回函数访问,但外部代码却无法访问。 // 下面是一个计数器的例子,每次当调用该函数时,它会产生一个递增的值: var setup1 = function() { var count = 0; return function () { return (count += 1); }; }; // 用法 var next = setup1(); console.log(next()); console.log(next()); console.log(next());
这篇主要介绍了回调函数和返回函数。这两种在实际的开发中都十分有价值。一定要认真看哦。下一篇,我们来学习下自定义函数以及即时函数。