2

回调函数模式

回调函数与控制反转

回调函数是程序设计的一种方法。这种方法是指,在传递了可能会进行调用的函数或对象之后,在需要时再分别对其进行调用。由于调用方与被调用方的依赖关系与通常相反,所以也成为控制反转(IoC,Inversion of Control)。
由于历史原因,在JavaScript开发中我们常常会用到回调函数这一方法,这是多种因素导致的。第一个原因是在客户端JavaScript中基本都是GUI程序设计。GUI程序设计是一种很适合使用所谓事件驱动的程序设计方式。事件驱动正是一种回调函数设计模式。客户端JavaScript程序设计是一种基于DOM的事件驱动式程序设计。
第二个原因是,源于客户端无法实现多线程程序设计(最近HTML5 Web Works支持多线程了)。而通过将回调函数与异步处理相结合,就能够实现并行处理。由于不支持多线程,所以为了实现并行处理,不得不使用回调函数,这逐渐成为了一种惯例。最后一个原因与JavaScript中的函数声明表达式和闭包有关。

JavaScript与回调函数

    var emitter = {
        // 为了能够注册多个回调函数而通过数组管理
        callbacks:[],
        // 回调函数的注册方法
        register:function (fn) {
            this.callbacks.push(fn);
        },
        // 事件的触发处理
        onOpen:function () {
            for (var f in this.callbacks) {
                this.callbacks[f]();
            }
        }
    };
    emitter.register(function () {alert("event handler1 is called");})
    emitter.register(function () {alert("event handler2 is called");})
    
    emitter.onOpen();    
    // "event handler1 is called"
    // "event handler2 is called"

定义的两个匿名函数就是回调函数,它们的调用由emitter.onOpen()完成。

 对emitter来说,这仅仅是对注册的函数进行了调用,不过根据回调函数的定义,更应该关注使用了emitter部分的情况。从这个角度来看,注册过的回调函数与之形成的是一种调用与被调用的关系。

上面的回调函数只是单纯的函数而不具有状态。如果回调函数具有状态,就能得到更为广泛的应用。下面我们把回调方改为了对象,于是emitter变为了接受方法传递的形式。

    function MyClass(msg) {
        this.msg = msg;
        this.show = function () {alert(this.msg+' is called');}
    }
    // 将方法注册为回调函数
    var obj = new MyClass("listener1");
    var obj2 = new MyClass("listener2");
    emitter.register(obj.show);
    emitter.register(obj2.show);
    
    emitter.onOpen();
    // undefined is called
    // undefined is called

我们发现,调用回调函数无法正确显示this.msg,错误原因在于JavaScript内的this引用。解决方法有两种,一种是使用bind,一种是不使用方法而是用对象进行注册。后者在JavaScript中并不常用。

    emitter.register(obj.show.bind(obj));
    emitter.register(obj2.show.bind(obj2));
    
    emitter.onOpen();
    // "listener1 is called"
    // "listener2 is called"

bind是ES5新增的功能,是Function.prototype对象的方法。bind的作用和apply与call相同,都是用于明确指定出方法调用时的this引用。对于函数来说,调用了bind之后会返回一个新函数,新的函数会执行与原函数相同的内容,不过其this引用是被指定为它的第一个参数的对象。在调用apply与call时将会立即调用目标函数,而在调用bind时则不会如此,而是会返回一个函数(闭包)。
如果使用了apply或call,就能对bind进行独立的实现。事实上在ES5才推出之前,在prototype.js等知名的库中就通过apply/call提供了bind自己的实现。

脑补的bind的内部实现?

    Function.prototype.bind = null;
    Function.prototype.bind = function (obj) {
        var f = this;
        return function () {
            f.call(obj);
        }
    }
    var obj = {
        x:"这是    obj.x    !!!",
        fn:function () {
            alert(this.x);
        }
    };
    var obj2 = {x:"obj2.x    对啦!!!"};
    
    var testfn = obj.fn.bind(obj2);
    testfn();    // "obj2.x    对啦!!!"

闭包与回调函数

    emitter.register(
        (function () {
            var msg = "closure1";
            return function () {alert(msg+" is called;")};
        }())
    );
    emitter.register(
        (function () {
            var msg = "closure2";
            return function () {alert(msg+" is called;")};
        }())
    )
    
    emitter.onOpen();
    // "closure1 is called"
    // "closure2 is called"

借助闭包,前面繁复的说明仿佛不在存在,可以很轻松的实现回调函数,并且还能像对象一样具有状态。


Queen
139 声望20 粉丝