this、call、apply

this

和其他语言大相径庭的是,JavaScript 的 this 总是指向一个对象,而具体指向哪个对象,是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。

this 的指向在实际应用中,大致可分为以下四种:

  • 作为对象的方法调用
  • 作为普通函数调用
  • 构造器调用
  • Function.prototype.call 或 Function.prototype.apply 调用
  1. 作为对象调用时,this 指向该对象。
  2. 作为函数调用时,this 总是指向全局对象。在浏览器的 JavaScript 中,这个全局对象是 window 对象。 在 ES5 的严格模式(strict)下,这种情况下的 this 被规定为不会指向全局对象,而是 undefined
  3. 构造器调用,大部分 JavaScript 函数都可以当做构造器使用,当被 new 运算符调用函数时,该函数会返回一个对象,通常情况下,构造器里的 this 就指向这个被返回的对象。 需要注意的是如果构造器显式地返回了一个 Object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们期待的 this
  4. Function.prototype.call 和 Function.prototype.apply 调用。 两者会动态地改变传入函数的 this。

This 的丢失问题

let obj = {
  name: 'jxd',
  getName: function () {
    return this.name;
  },
};
console.log(obj.getName()); // jxd
let getName2 = obj.getName;
console.log(getName2()); // undefined

在调用 getName2 时,此时是普通函数调用方式,this 是指向全局 window 的,所以程序的执行结果是 undefined

call 和 apply

在 JavaScript 的设计模式实现中,两个方法的应用非常广泛,掌握这两个方法是成为 JavaScript 工程师的重要一步。

Function.prototype.call 和 Function.prototype.apply 它们的作用于洋,区别在于传入参数不同。

  • apply apply 接受两个参数,第一个参数指定了函数内 this 的指向。第二个参数为一个带下标的集合,apply 把这个集合中的元素作为参数传递给被调用的函数。

    let func = function (a, b, c) {
      console.log(a, b, c);
      console.log(this.name);
    };
    let obj = {
      name: 'jxd',
    };
    func.apply(obj, [1, 2, 3]);
    // 1 2 3
    // jxd
  • call call 传入的参数数量不固定,第一个参数与 apply 一样,欧式当前函数体内的 this 指向,从第二个参数开始,每个参数被依次传入函数。

    let func = function (a, b, c) {
      console.log(a, b, c);
      console.log(this.name);
    };
    let obj = {
      name: 'jxd',
    };
    func.apply(obj, 1, 2, 3);
    // 1 2 3
    // jxd

需要注意的是,如果我们传入的第一个参数是 null,函数体内的 this 会指向默认的宿主对象,在浏览器中则是 window。

let func = function (a, b, c) {
  console.log(this === window);
};
func.apply(null, [1, 2, 3]); // true

call 和 apply 的用途

  1. 改变 this 指向 比如我们想把 document.getElementById 封装成一个函数,那么作为普通函数,getId 的 this 是 undefined,因为 getElementById 内部也有用到 this,就会出现问题。所以需要改变 this 的指向。

    document.getElementById = (function (func) {
      return function () {
        return func.apply(document, arguments);
      };
    })(document.getElementById);
    let getId = document.getElementById;
    let div = getId('div1');
    console.log(div.id); // div1
  2. Function.prototype.bind 大部分浏览器都内置了 Function.prototype.bind,用来指定函数内部的 this 指向,我们可以用 call 或者 apply 来模拟一个。 比如这里就相当于把 func 函数“包装”了一下,替换成了另一个函数。

    Function.prototype.bind = function (context) {
      let self = this; // 作为对象的方法被调用,指向对象
      return function () {
        return self.apply(context, arguments);
      };
    };
    let obj = {
      name: 'jxd',
    };
    let func = function () {
      console.log(this.name);
    }.bind(obj);
    func();
  3. 借用其他对象的方法

  • 借用构造函数 比如在 B 的构造函数中,使用 apply 调用 A 的构造函数,这样 B 就可以使用到 A 里的属性,实现类似继承的效果。

    let A = function (name) {
      this.name = name;
    };
    let B = function () {
      A.apply(this, arguments);
    };
    B.prototype.getName = function () {
      return this.name;
    };
    let b = new B('jxd');
    console.log(b.getName());
  • 借用内置方法 比如函数的 arguments 是一个类数组对象,有下标。但是不能进行排序和添加操作。我们可以借用 Array.prototype 上的方法。

    (function () {
      Array.prototype.push.call(arguments, 3);
      console.log(arguments);
    })(1, 2);