函数绑定

按钮的点击事件

给按钮添加点击事件的方法一般就是两种。

第一种直接在标签中直接添加onclick回调即可。

另一种则是在js中动态添加事件。

这里主要记录的是第二种。

在js中动态添加事件一般都是这种格式如:

function test1(){
	console.log("hello")
}
var btn = document.getElementById("my-btn"); 
btn.addEventListener("click", test1); 

看起来也是一切正常。但是当所绑定这个函数需要参数时,就出现了问题。

如果把之前的代码改成这样:

function test1(str){
    console.log(str)
}
var btn = document.getElementById("my-btn"); 
btn.addEventListener("click", test1("测试")); 

在还没有点击按钮的时候,点击事件就已经触发了。这是什么原因呢?我在test1方法中打印了this对象,指向window全局对象的,我的理解是在window中执行了test1方法,这样的话当进入浏览器时函数就已经被执行了,而不带参数的test1方法在代码中是以test1的形式绑定在按钮上的而不是test1(),所以仅仅起到一个标识的作用用来找到对应的函数,当它以test1()的形式来给按钮绑定不带参数的方法时同样也会出现没有点击也会被执行事件。

最容易想到的例子是,一个方法定义之后要调用他,就在他后面加个(),如function a( ){...},要使用时,则a(),

而function b(str){ ... } 则调用时 b('teststr')。

除此之外,除了刚进入页面就会执行之外,再此点击按钮也没有再此执行,原因是因为addEventListener是为了添加回调函数,而无论是test1()还是test1("测试")做的事情都只是执行函数不是返回一个函数,所以也就没有绑定成功事件。

而如果要避免出现类似问题,要怎么做呢?

使用匿名函数可以实现。

        function test1(str){
            console.log(this)
            console.log(str)
        }
        var btn = document.getElementById("my-btn"); 
        btn.addEventListener("click",function(e){
            console.log(e)
            test1("测试")
            test1.call(this,"测试")
        }); 

这样就可以解决之前的问题。同时也可以通过call将test1的this指向绑定事件的按钮,也通过参数拿到事件对象,这样才是最理想的状态。

但是在实际开发中,很多方法并不会单独写出来,而是写在某个对象中。

var obj = { 
 message: "Event handled", 
 test1: function(){ 
    console.log(this)
 	console.log(this.message); 
 } 
}; 

这时我如果用obj.test1绑定在btn按钮上,点击按钮输出的是undefined,这时this指向的是btn,btn并没有message属性。原因是这个问题在于没有保存 obj.test1()的环境,所以 this 对象最后是指向了 DOM 按钮而非 test1解决方法和之前一样使用可以如下面例子所示,使用一个匿名函数实现闭包来修正这个问题。

btn.addEventListener("click",function(e){
    obj.test1()
}); 

如果这个obj.test需要多个参数呢?那么可以直接修改方法,并在调用时添加即可。如果也需要获取事件对象呢,则可以从匿名回调函数中拿到事件对象e,在传入方法中。

但是多次创建闭包会使代码的调试和理解变得困难,所以可以手动封装一个bind函数,实现以上功能。

封装一个bind函数

以上解决方式的核心都是通过闭包指向正确的执行环境并传递正确的参数。

则可以封装出一个基本的bind函数。fn为需要添加的回调函数,context可以理解指定的this的指向。

function bind(fn, context){ 
    return function(e){ 
    	return fn.apply(context, arguments); 
    }; 
} 

这里的arguments实际上就是,点击传入事件对象e,又通过apply传给fn的函数中,这样就在fn拿到了事件对象。

实际使用时可

btn.addEventListener("click",bind(obj.test1,obj)); 

但是如果既要使用封装的bind函数,而fn也是一个需要参数的方法而之前拿到的事件对象也不可以丢失时这样怎么实现呢?

这里就需要函数的柯里化。

函数的柯里化

这里主要是通过获取参数类数组对象arguments实现不同层函数之间参数的拼接。

整理如下:

function bind(fn,context){ 
    let args = Array.prototype.slice.call(arguments, 2);
    return function(){ //事件对象e在这里拿到
        var innerArgs = Array.prototype.slice.call(arguments); 
        var finalArgs = args.concat(innerArgs); 
        return fn.apply(context, finalArgs); 
    }; 
} 

其中2代表从第三个参数开始截取参数,用来作为fn所需要的参数,而在匿名函数中的arguments则是点击事件的事件对象e,之后通过concat实现拼接,就拿到了全部的参数,并可以在fn得到使用。

使用如下:

var obj = { 
    message: "Event handled", 
    test1: function(a,b,event){ 
        console.log(a,b);
        console.log(event);
    } 
}; 
btn.addEventListener("click",bind(obj.test1,obj,1,2)); 

而实际上JavaScript已经帮我们实现好了bind函数,同时也实现相应的柯里化。

使用的方式也是非常常见了:

btn.addEventListener("click",obj.test1.bind(obj,1,2));

其实在日常的学习和开发中,bind函数会经常用到,但其实背后的实现方式并没有了解过,希望通过这次的记录,在后面使用到bind函数时能够记忆犹新。

Last Updated:
Contributors: jackysei