JavaScript函数的call,apply和bind

函数里的this指针

要明白call,apply和bind,那得先晓得JavaScript里的this指针。JavaScript里任何函数的实行都有一个上下文(context),也就是JavaScript里常常说到的this指针,比方:

var obj = {
  a: 3,
  foo: function() {
    console.log(this.a);
  }
}

obj.foo();
// 实行结果:打印出a的值3

这里foo的实行context就是obj,即this指针的指向。固然函数也能够直接定义,不加任何容器:

var bar = function() {
  console.log(this.b);
}

bar();  // 等价于global.bar()

假如我们挪用bar(),那就相当于挪用global.bar(),也就是挪用在了一个全局的object上。在差别的环境里这个全局object不一样,在浏览器里是window,在node里则是global,这都是JavaScript实行环境预设的。现实上我们定义的bar函数自身就是定义在了这个global上,即等价于:

global {
  bar: function() {
    console.log(this.b)
  }
}

所以直接实行bar的时刻,context固然是global了。这和上面的obj和foo的例子是一样的。

实在context的观点关于任何面向对象的言语都是一样的,比方Java和C++,它们的Class的成员函数在挪用的时刻第一个参数永久是一个隐式的this指针,然后才是真正的参数。谁人this指针必需指向一个详细的这个Class的实例object。

然则Java和C++如许的言语和JavaScript不一样的处所就在于,它们对函数的挪用更严厉。它们有明白的Class和成员函数的定义,只需Class的实例object才挪用这个Class的成员函数。而JavaScript作为一门函数式编程言语则比较自在,函数的挪用能够恣意指定上下文context。比方上面的foo和bar,我们能够并不挪用在obj和global上,而是自行指定,这就须要用到call和apply。

用call和apply挪用函数

JavaScript里用call和apply来指定函数挪用的context,即this指针的指向。call和apply都是定义在Function的prototype上,所以任何函数都继续了它们,能直接运用,比方我们定义函数func:

function func(v1, v2) {
  console.log(this.a);

  console.log(v1);
  console.log(v2);
}

然后挪用func:

var x = {
  a: 5
}

func.call(x,7, 8);

// 实行结果:
// 打印出x.a的值5
// 打印出v1的值7
// 打印出v2的值8

这里我们用call来挪用func函数,call的第一个参数等于要绑定的上下文context,所以函数里的this.a就成了x.a,而x背面传入的参数就是func函数自身的参数v1和v2。

apply的用法也是相似的,只不过apply把x背面的参数组织成一个数组:

func.apply(x,[7, 8]);

这里正由于我们用call和apply指定了func挪用的context为x,因而它才够打印出this.a的值,由于x里定义了a。假如直接挪用func(),它就会挪用在global上,此时a并没有定义,则会打印出undefined。

bind的用法

有时刻我们想指定函数挪用的context,但并不想马上挪用,而是作为callback传给他人,这在JavaScript里屡见不鲜,由于到处都会有须要给监听事宜绑定callbak函数,这时刻call和apply就不能用了,须要用到bind。

var funcBound = func.bind(x);

funcBound(7, 8);
// 等价于:
// func.call(x, 7, 8)

一样是上面的func函数,正如bind的字面意义,此次我们将x绑定到了func上,获得一个新的函数funcBound,此时funcBound的上下文就已被指定为了x,然后它再直接挪用参数7和8,那末结果就相当于之前的func.call(x, 7, 8)了。

bind也能够传入多个参数,此时第一个参数绑定为this,背面的参数则绑定为一般参数,比方:

var funcBound = func.bind(x, 7);

funcBound(8);
// 等价于:
// func.call(x, 7, 8)

与bind作为比较,在JavaScript里为了掌握函数的挪用者,一种很轻易的做法就是用闭包(Closure),由于闭包会保留任何外部的被运用到的变量的reference。不过闭包并没有转变函数实行的上下文,也就是说this指针并没有转变,它能转变的只是函数体里某些详细的变量的指向,这现实上是一种强耦合,须要你在写函数的时刻自身就用到外部定义好的一些变量,这是一种不可扩大的函数定义体式格局。而假如是一个很通用的函数,要想完成this指针的恣意绑定,像call和apply那样,则必需用bind。

bind在C++ 11里也有相似的用法,也是C++完成callback的一种重要体式格局,只不过就像之前说的,C++对范例的搜检更严厉,绑定的不论是this照样别的参数都必需严厉相符这个函数定义的形参的范例。而关于JavaScript,你能够随便给任何函数绑定任何参数,而且如许的绑定常常是隐式的,这也是形成JavaScript里this指针很轻易殽杂的缘由,由于它的函数挪用比较自在,而且每每许多时刻底层挪用的细节我们是不晓得的。

完成一个简朴的bind

实在我们很轻易用call和apply完成一个简朴的bind,来加深对bind的明白。这里假定你对JavaScript的Function,Array以及闭包有肯定相识。

function myBind() {
  // bind的挪用者,即底本的函数。
  var func = this;
  
  // 猎取bind的参数列表。
  var args = Array.prototype.slice.call(arguments);
  if (args.length === 0) {
    // 假如未传入任何参数,那就直接返回底本的函数。
    return func;
  }
  
  // 第一个参数是实行上下文context。
  var context = args[0];
  // 背面的参数是一般的挪用传参,从第二个参数最先。
  var boundArgs = args.slice(1);

  // 定义bound后的函数,这里用到了Closure。
  var boundFunc = function() {
    // 猎取现实挪用时传入的参数,而且拼接在之前的boundArgs背面,成为真正的完全参数列表。
    var args = Array.prototype.slice.call(arguments);
    var allArgs = boundArgs.concat(args);
    
    // 这里用apply来挪用原始的函数func,实行上下文指向context,并传入完全参数列表。
    return func.apply(context, allArgs);
  }

  // 返回bound函数。
  return boundFunc;
}

如许就完成了一个简朴版本的bind,它已能够完成bind的基本功用,即绑定context和参数列表。详细的道理都在每一步的诠释里写出来了,我就不做过多诠释了。假如把它加到Function的prototype里,那一个一般的函数比方foo就可以够运用foo.myBind(…)来完成bind相似的功用。
——-

有一个风趣的题目,就是假如对一个函数举行屡次bind会发作什么事情?会不会context被不断更新?答案是不会,只需第一次bind的context会作为末了挪用的现实的this指针。关于这一点,只需对比上面的完成就可以明白,不论bind多少次,最里层的apply永久只作用在第一次的传入的context,也就是说原始函数func的挪用对象只能是第一次的谁人context。

一样,关于一个bind后的函数运用call或许apply,也没法转变它的实行context,道理和上面是一样的。在现实编程中也不会有人如许写,然则初学者有时刻可能会不小心犯下如许的毛病。现实上我就是初学者。。。也是在这里纪录一下我学JavaScript过程当中的一些题目和主意。

    原文作者:hyuan
    原文地址: https://segmentfault.com/a/1190000013366938
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞