JavaScript - 回调函数

作者: NARUTO_86 | 来源:发表于2014-08-31 08:48 被阅读2949次

    这里我想跟你聊聊我理解的回调函数(callback)。

    回调函数,我觉得可以理解成作为参数传递的函数对象。因为在 JavaScript 中函数有比较高的等级(是一种基本类型,嗯,应该可以这样讲吧),可以独立地进行使用。

    而在 Java 中,可能只有作为类的方法的“函数”存在吧,不存在可以单独使用的函数。当然在 Java 中,我“揣测”函数,或者叫方法(method)更合适,可能只有被调用的使用方式,例如: foo.method()

    在 JavaScript 中,函数还是挺独立的。例如,尽管有的时候一个函数在声明的时候就是作为某个对象的方法的形式,但仍然可以撇开这个对象来使用。这里用到了函数的 .call().apply() 方法(这里就叫方法吧,不再绕来绕去了)。例如:

    var Person = function (name) {
        this.name = name;
        this.getName = function () {
            return this.name;
        };
    };
    

    我先用自己的话跟你大致说下上面的代码里面做了什么事情,有些东西是我之前的文章还没有讲到的,不过相信你可能也都懂的:

    1. 首先,我声明了一个变量和一个函数,函数本身没有名称,将其作为值赋给了变量 Person,这是最主要的结构;

    2. 然后,再来看这个匿名函数的内部,我给 this 对象(且这么叫吧)设置了两个属性,属性 name 的值为匿名函数的参数 name,属性 getName 的值为一个匿名函数;

    3. 最后,我们看下 getName 对应的匿名函数干了什么:它返回了 this 的属性 name 的值。

    对于 Person 的使用方式,之前提到过(不过定义的形式可能稍有不同,这不是重点),可以这样用:

    var me = new Person("luobo");
    me.getName(); // "luobo"
    

    这里说了挺多题外的东西,现在我们来看上面 me 这个对象的方法 getName()。尽管形式上是对象的方法,而且在一开始定义的时候就确定了其方法的目的(这种 this.foo = ... 的方式我会在讲继承的时候再跟你多聊一些),但是由于其函数的本质,仍然可以单独来使用:

    me.getName.call({ name: "Mickey" }); // "Mickey"
    

    注意,这里 me.getName 只是为了引用到实际的函数(对象),以 .call() 来调用时传入了新的“上下文对象” { name: "Mickey" },而这个上下文对象在函数执行时会替换函数内部使用的 this,有点了解了吗?

    所以,实际上对于上文中声明的 Person 所对应的匿名函数,也可以这样来使用:

    var me = {};
    Person.call(me, "luobo");
    me.getName(); // "luobo"
    

    再引申一点来讲,使用 new Person() 这样的形式来获得一个对象,那么在 Person 内部的 this 就是对应这个即将返回的对象 。(严格来说不完全是,在 Person 对应的匿名函数有明确定义的其他返回值时就不再返回这个 this 对象啦)

    虽然是要讲回调函数,可是我花了好多时间来讲每个函数都可以被独立调用这件事,还说到函数中的 this 在函数执行时可以明确指定(通过 .call() 传入的第一个参数即作为函数内部 this 所指向的对象),这些都是会用到的。

    下面我举个使用回调函数的栗子:

    $("#myDiv").on("click", function () {
        this.innerHTML = "you clicked me!";
    });
    

    这个栗子中,我们使用 jQuery 给一个 id 为 myDiv 的元素绑定了 click 事件的处理程序,这里的事件处理程序就是一个回调函数。当然回调函数并非是一种特殊类型的对象,其实就是普通的函数,但是被作为其他函数的参数传递,在某个时刻才会被选择性地使用。

    单纯看回调函数不是特别有趣,不过你应该注意到上面这个回调函数的定义中使用 this,那么这个 this 又是指向什么对象呢?

    实际上,这是由 jQuery 提供的机制,在事件处理程序被调用时,this 会指向事件的目标对象,这里也就是被点击的 myDiv 元素,而 HTML 元素有 innerHTML 属性,就是上面的使用方式了。这是怎么实现的呢,合理揣测下的话,肯定是这个回调函数在调用是被显式指定了“上下文对象” this,有可能就是通过 .call().apply() 的方式。

    另外,由于回调函数会在什么时候被执行,可能是不确定,甚至也可能永远不会被执行。而且在上面的这种情况下,这个回调函数甚至要在未来的某个时候才会执行,这就有了一个“异步”的感觉,也就是说代码不是从上到下依次执行,前面的代码执行完毕后面的才会执行(这个可以叫做“同步”啦)。

    在使用 Ajax 时,我们可以指定请求是同步还是异步的方式,同步的方式下比较好理解,一定是这个请求接收到服务器响应或者超时失败后,后面的代码才会执行。这种情况下我们编写需要在一个 Ajax 请求后才能做的时候,例如 alert("Ajax 请求已完成!"),是比较容易的。不过坏处是代码在执行到这里的时候会等待 Ajax 请求被响应,一切都停了下来。如果不想让世界暂停下来,就可以使用 Ajax 异步的模式(这也是通常的使用方式),然后把想要做的事情以回调函数的形式包装一下,等待请求在未来有个结果(响应、超时、意外终止等)后执行。举个栗子:

    $.ajax("/foo.jsp").done(function () {
        alert("成功!");
    }).fail(function () {
        alert("失败!");
    });
    

    这里分别为请求成功和失败两种情况指定了回调函数。

    关于回调函数,咱们就聊这么多吧。

    扩展:

    • 关于“异步”执行,有没有考虑过专门去使用它的情况?
      例如,代码执行时间预期会很长,为避免浏览器不响应用户交互,人为地将代码分为多个部分分别执行,从而使得每个部分执行时间不会很长。
    • 主动地让代码“异步”执行的方法?
      常见的有 setTimeout,另外还有借助浏览器的事件机制实现的,如为新创建的 img 元素指定 onload 事件的方式等等。
    • 异步执行的代码多层嵌套的处理
      因为要以回调函数的方式来编写异步执行代码,可能会很多级匿名函数互相嵌套的情况,代码可读性会比较差。这种情况下一个选择是可以使用一些第三方的库,如 when.js。其实 jQuery 也提供了异步机制,可以看下 deffered,我们常用的 jQuery 的 ajax 函数也基于这个东东改造过。

    接下来还想跟你聊聊:

    • JavaScript - 作用域和“闭包”
    • JavaScript - 继承和“类”

    相关文章

      网友评论

      • 仅愚:因为javascript是单线程的,它需要尽可能地不阻塞。回调函数往往和异步的概念相关,而异步又是javascript事件驱动机制的基石。《精粹》这本书说,定义了一个回调就是定义了一个“事件”。一个事件总是在线程空闲的时候才能“准时”触发,否则将会延迟。
      • NARUTO_86:@LostAbaddon 我只是听说过 C/C++ 的函数指针,对.net的 delegate 不怎么了解,Java 的 Runable 就完全没听说过了^_^。所以尽管是写给 Java 程序员同事看的,我对 Java 程序员同事在Java中的应用并不了解,汗....
      • LostAbaddon:甚至于,C和C++里使用void来做delegate的做法也可以一起聊聊,哈哈~~
      • LostAbaddon:这部分可以结合.net里的delegate以及Java里的Runable多谈谈

      本文标题:JavaScript - 回调函数

      本文链接:https://www.haomeiwen.com/subject/hejytttx.html