Call、apply、bind 的用法

48 阅读7分钟

this 的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把 this 固定下来,避免出现意想不到的情况。JavaScript 提供了 call、apply、bind 这三个方法,来切换/固定 this 的指向。

call

函数实例的 call 方法,可以指定函数内部 this 的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true

上面代码中,全局环境运行函数 f 时,this 指向全局环境(浏览器为 window 对象);call 方法可以改变 this 的指向,指定 this 指向对象 obj,然后在对象 obj 的作用域中运行函数 f。

call 方法的参数,应该是一个对象。如果参数为空、null 和 undefined,则默认传入全局对象。

var n = 123;
var obj = { n: 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

上面代码中,a 函数中的 this 关键字,如果指向全局对象,返回结果为 123。如果使用 call 方法将 this 关键字指向 obj 对象,返回结果为 456。可以看到,如果 call 方法没有参数,或者参数为 null 或 undefined,则等同于指向全局对象。

如果 call 方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入 call 方法。

var f = function () {
  return this;
};

f.call(5)
// Number {[[PrimitiveValue]]: 5}

上面代码中,call 的参数为 5,不是对象,会被自动转成包装对象(Number 的实例),绑定 f 内部的 this。

call 方法还可以接受多个参数。call 的第一个参数就是 this 所要指向的那个对象,后面的参数则是函数调用时所需的参数。

function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3

上面代码中,call 方法指定函数 add 内部的 this 绑定当前环境(对象),并且参数为 1 和 2,因此函数 add 运行后得到 3。

call 方法的一个应用是调用对象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false

// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

上面代码中,hasOwnProperty 是 obj 对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call 方法可以解决这个问题,它将 hasOwnProperty 方法的原始定义放到 obj 对象上执行,这样无论 obj 上有没有同名方法,都不会影响结果。

利用原生js实现call方法:

function call(context, ...params) {
  context == undefined ? (context = window) : null;
  // context不能是基本数据类型值,如果传递是值类型,我们需要把其变为对应类的对象类型
  if (!/^(object|function)$/.test(typeof context)) {
    if (/^(symbol|bigint)$/.test(typeof context)) {
      context = Object(context);
    } else {
      context = new context.constructor(context);
    }
  }
  let key = Symbol("context");
  let result = null;
  context[key] = this;
  result = context[key](...params);
  delete context[key];
  return result;
};

apply()

apply 方法的作用与 call 方法类似,也是改变 this 指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

func.apply(thisValue, [arg1, arg2, ...])

apply 方法的第一个参数也是 this 所要指向的那个对象,如果设为 null 或 undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在 call 方法中必须一个个添加,但是在 apply 方法中,必须以数组形式添加。

function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2```
  1. 找出数组最大元素,JavaScript 不提供找出数组最大元素的函数。结合使用 apply 方法和 Math.max 方法,就可以返回数组的最大元素。

    var a = [10, 2, 4, 15, 9];
    Math.max.apply(null, a) // 15
    
  2. 转换类似数组的对象,利用数组对象的 slice 方法,可以将一个类似数组的对象(比如 arguments 对象)转为真正的数组。

    Array.prototype.slice.apply({0: 1, length: 1}) // [1]
    Array.prototype.slice.apply({0: 1}) // []
    Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
    Array.prototype.slice.apply({length: 1}) // [undefined]
    

    上面代码的 apply 方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有 length 属性,以及相对应的数字键。

  3. 绑定回调函数的对象

    var o = new Object();
    
     o.f = function () {
     console.log(this === o);
     }
    
     var f = function (){
     o.f.apply(o);
     // 或者 o.f.call(o);
     };
    
     // jQuery 的写法
     \$('#button').on('click', f);
    

    上面代码中,点击按钮以后,控制台将会显示 true。由于 apply 方法(或者 call 方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的 bind 方法。

bind()

bind 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数。

var counter = {
  count: -1,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc.bind(counter);
func();
counter.count // 1

上面代码中,counter.inc 方法被赋值给变量 func。这时必须用 bind 方法将 inc 内部的 this,绑定到 counter,否则就会出错。

this 绑定到其他对象也是可以的。

var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var obj = {
  count: 100
};

var func = counter.inc.bind(obj);
func();
obj.count // 101

上面代码中,bind 方法将 inc 方法内部的 this,绑定到 obj 对象。结果调用 func 函数以后,递增的就是 obj 内部的 count 属性。

bind 还可以接受更多的参数,将这些参数绑定原函数的参数。

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20

上面代码中,bind 方法除了绑定 this 对象,还将 add 函数的第一个参数 x 绑定成 5,然后返回一个新函数 newAdd,这个函数只要再接受一个参数 y 就能运行了。

如果 bind 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为 window)。

利用原生js实现bind:

function bind(context = window, ...params) {
  let _this = this;
  return function anonymous(...inners) {
    _this.apply(context, params.concat(inners));
  };
}

bind 方法有一些使用注意点。

  1. 每一次返回一个新函数,bind 方法每运行一次,就返回一个新函数,这会产生一些问题。

     var obj = {
       init: 1,
       add: function(a, b) {
           return a + b + this.init;
       }
     }
     obj.add(1, 2); // 4
    
     var plus = obj.add;
     plus(3, 4); // NaN,因为 this.init 不存在,这里的 this 指向 window/global
    
     plus.call(obj, 3, 4) // 8
     plus.apply(obj, [3, 4]); // 8, apply 和 call 的区别就是第二个参数为数组
     plus.bind(obj, 3, 4); // 返回一个函数,这里就是 bind 和 call/apply 的区别之一,bind 的时候不会立即执行
     plus.bind(obj, 3, 4)(); // 8
    
  2. 结合回调函数使用,回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含 this 的方法直接当作回调函数。

    var counter = {
      count: 0,
      inc: function () {
        'use strict';
        this.count++;
      }
    };
    
    function callIt(callback) {
      callback();
    }
    
    callIt(counter.inc.bind(counter));
    counter.count // 1
    

    上面代码中,callIt 方法会调用回调函数。这时如果直接把 counter.inc 传入,调用时 counter.inc 内部的 this 就会指向全局对象。使用 bind 方法将 counter.inc 绑定 counter 以后,就不会有这个问题,this 总是指向 counter。

  3. 还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的 this 指向,很可能也会出错。

    var obj = {
      name: '张三',
      times: [1, 2, 3],
      print: function () {
        this.times.forEach(function (n) {
          console.log(this.name);
        });
      }
    };
    
    obj.print()
    // 没有任何输出
    

    上面代码中,obj.print 内部 this.times 的 this 是指向 obj 的,这个没有问题。但是,forEach 方法的回调函数内部的 this.name 却是指向全局对象。解决这个问题,也是通过 bind 方法绑定 this。

    obj.print = function () {
      this.times.forEach(function (n) {
        console.log(this.name);
      }.bind(this));
    };
    
    obj.print()
    // 张三
    // 张三
    // 张三