JavaScript 语法基础—— this

this 的指向

除去不常用的 with 和 eval 的情况,具体到实际应用中,this 的指向大致可以分为以下 4 种。

  • 作为对象的方法调用,当函数作为对象的方法被调用时,this 指向该对象。
var module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};
console.log(module.getX()); // 42
  • 作为普通函数调用,当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指向全局对象。在浏览器的 JavaScript 里,这个全局对象是 window 对象。本质上其实我们也可以理解为此时作为全局对象的方法。
var name = "global";
function getName() {
  this.name = "inner";
}
getName();
console.log(name); // inner
  • 构造器调用,构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象。
function MyClass() {
  this.name = "sven";
}
var obj = new MyClass();
console.log(obj.name); // sven

但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this:

var MyClass = function() {
  this.name = "sven";
  return {
    name: "anne"
  };
};
var obj = new MyClass();
console.log(obj.name); // anne

如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题。

  • Function.prototype.call 或 Function.prototype.apply 调用。

跟普通的函数调用相比,用 Function.prototype.call 或 Function.prototype.apply 可以动态地 改变传入函数的 this。

var obj1 = {
  name: "sven",
  getName: function() {
    return this.name;
  }
};
var obj2 = {
  name: "anne"
};
console.log(obj1.getName()); // sven
console.log(obj1.getName.call(obj2)); // anne

call 和 apply 方法能很好地体现 JavaScript 的函数式语言特性,在 JavaScript 中,几乎每一次编写函数式语言风格的代码,都离不开 call 和 apply。

call 和 apply

apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

var module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

var unboundGetX = module.getX;
console.log(unboundGetX()); // undefined

var boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42

大部分高级浏览器都实现了内置的 Function.prototype.bind,用来指定函数内部的 this 指向,即使没有原生的 Function.prototype.bind 实现,我们来模拟一个也不是难事,代码如下:

Function.prototype.bind = function() {
  var self = this,
    context = [].shift.call(arguments),
    args = [].slice.call(arguments);
  return function() {
    // 返回一个新的函数,需要绑定的 this 上下文,剩余的参数转成数组
    // 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this
    // 并且组合两次分别传入的参数,作为新函数的参数
    return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
  };
};

[].slice.call(arguments)Array.prototype.slice.call(arguments) 能够将 arguments 转成数组。

测试如下:

var obj = {
  name: "sven"
};
var func = function(a, b, c, d) {
  console.log([a, b, c, d]); // 输出:[ 1, 2, 3, 4 ]
}.bind(obj, 1, 2);
func(3, 4);

对于 Function.prototype.bind 完整严谨的实现可以参考 MDN 上的 Ployfill。

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError(
        "Function.prototype.bind - what is trying to be bound is not callable"
      );
    }

    var aArgs = Array.prototype.slice.call(arguments, 1),
      fToBind = this,
      fNOP = function() {},
      fBound = function() {
        return fToBind.apply(
          this instanceof fNOP ? this : oThis,
          aArgs.concat(Array.prototype.slice.call(arguments))
        );
      };

    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
}