除了 call,JS 还有哪些强大的函数绑定方式?

407 阅读5分钟

前言:探索 JavaScript 函数绑定的多样世界

在我深入研究 setTimeout 时,遇到了一个常见的挑战:如何确保在异步回调中正确地绑定 this 的指向。最初,我总是依赖于 call 方法来解决这个问题,因为它简单直接,能够立即执行函数并显式地设置 this。然而,随着项目的复杂度逐渐增加,我发现仅仅依靠 call 并不足以应对所有情况。这让我开始思考:JavaScript 中是否还有其他方法可以实现类似的 this 绑定?它们与 call 又有着怎样的区别和各自的优势呢?

在这篇文章中,我们将一起探讨这些不同的函数绑定方法,包括:

  • apply:与 call 类似,但它接受参数数组,非常适合处理未知数量或动态数量的参数。
  • call:允许你显式地设置函数内部的 this 值,并立即执行函数,适用于已知参数数量和顺序的情况。
  • bind:创建并返回一个新的函数,该函数的 this 被永久绑定到提供的对象,不会立即执行,非常适合预设部分参数和处理异步回调。

执行时间:

  • call 和 apply:两者都会立即调用函数,并且可以在调用时指定 this 的值。

  • bind:创建并返回一个新的函数,但不会立即执行。新函数的 this 被永久绑定到提供的对象,可以稍后调用。

案例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
<script>
var obj = {
  name: "Cherry",
  func1: function() {
    console.log(this.name)
  },
  func2: function() {
    setTimeout(function() {
      this.func1()
    }.call(obj), 10000) // 立即执行
  },
  func3: function() {
    setTimeout(function() {
      this.func1()
    }.apply(obj), 1000000) // 立即执行
  },
  func4: function() {
    setTimeout(function() {
      this.func1()
    }.bind(obj), 1000) 
  }
}
obj.func2()
obj.func3()
obj.func4()
</script>
</body>
</html>

结果:

在这个代码片段中,func2func3func4 都试图通过 setTimeout 来延迟执行一个匿名函数,并确保该匿名函数内部的 this 指向 obj。然而,func2func3 的实现存在一个问题:.call(obj).apply(obj)立即执行匿名函数,而不是将其作为回调传递给 setTimeout,所以会马上输出obj.name, 而.bind会返回一个新的函数不会马上执行,会传递给 setTimeout等待时间后,再输出。

参数传入

call的参数传入

案例:

var a={
    name:"zhangsan",
    fn:function(a,c){
        console.log(this.name);
        console.log(a+c);
    }
}

var b=a.fn;
b.call(a,1,2);

解释: 这段代码定义了一个对象 a,它有一个属性 name 和一个方法 fn。方法 fn 接受两个参数 ac,并打印出当前上下文(即调用时的 this)的 name 属性以及这两个参数的和。

接着,创建了变量 b,它引用了对象 afn 方法。此时,b 是一个独立于对象 a 的函数引用。如果直接调用 b(),那么在非严格模式下,this 将指向全局对象(例如浏览器中的 window),而在严格模式下,this 将是 undefined。因此,直接调用 b() 不会访问到对象 aname 属性。

通过call:

  1. b.call(a, 1, 2) 中的 call 方法让 b 函数内部的 this 指向了对象 a
  2. 参数 1 和 2 分别作为 fn 方法的参数 a 和 c 的值传递给函数。
  3. 当 fn 方法被执行时,它首先打印 this.name,也就是对象 a 的 name 属性 "zhangsan"
  4. 然后它打印参数 a 和 c 的和,即 1 + 2 的结果 3

最终: call 的第一个参数总是用来设置函数执行时的 this 值,而后续的参数则是目标函数的实际参数。每个参数都是单独列出的,以逗号分隔。call 一个个给 call(thisBinder,a,b,c,....)

apply的参数传入

案例

var a={
    name:"zhangsan",
    fn:function(a,c){
        console.log(this.name);
        console.log(a+c);
    }
}

var b=a.fn;
b.apply(a,[1,2]);

解释: 其他和call 其实一样,我就不做过多的赘述,我们可以看到b.apply(a,[1,2]);这段代码就是applycall 的区别所在:apply 接受一个参数数组(或类数组对象) 作为它的第二个参数,这个数组中的元素将作为参数传递给目标函数。apply(thisBinder,[a,b,c,....],[a,b,c,....])

bind的不同之处:

讲实话,bind有点像call的私生子,和call有点像,但是有自己的不同的地方。

案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        var a={
            name:"zhangsan",
            fn:function(a,b){
                console.log(this.name);
                console.log(a+b);
            }
        }
        var b=a.fn;// 普通函数
       
       console.log( b.bind(a,1,2) ) // 返回新的函数,但是不立即执行, 酷似call() 
          b.bind(a,1,2)()
    </script>
</body>
</html>

输出:


ƒ (a,b){
                console.log(this.name);
                console.log(a+b);
            }
 zhangsan
 3

结果: 我们可以看到,.bind 的传入方式和call一样,但是它只返回了一个新的函数,没有马上执行。 这意味着你可以延迟执行,甚至可以在后续调用中添加更多参数。

总结:使用时机

  • call 和 apply:当你需要立即执行函数并显式设置 this 值时,根据是否已有参数数组来选择 call 或 apply
  • bind:当你需要创建一个具有特定 this 绑定和预设参数的新函数,并且可能在不同的时间点调用它时,bind 是更好的选择,特别是对于异步回调和事件监听器等场景。

有趣的现象:

当我用,传入apply参数的方式,传给call,会发生什么呢?通过这段代码大家可以猜猜结果是什么?

var a={
    name:"zhangsan",
   
    fn:function(a,b){
        console.log(this.name);
        console.log(a+b);
    }
}
var b=a.fn;

 b.call(a,[1,2])

最终结果:

zhangsan
1,2undefined

为什么是这个是1+2undefined呢?

由于传递的是一个数组 [1,2] 作为一个单独的参数,这意味着在函数 fn 内部,a 参数会接收到整个数组 [1,2],而 b 参数将是 undefined(因为没有提供第二个参数)。因此,console.log(a + b) 实际上会尝试将数组 [1,2]undefined 字符串拼接,因为 JavaScript 无法直接相加数组和 undefined时,会自动转化为字符串。

实际运行: a.toString()+String(b)

为什么b不使用toString(),就留给大家思考了。

如果本文对你有帮助,可以留下小小的赞,如果有错误,敬请指出,谢谢您的指教。