this的指向

this关键字常用于表示代码的当前上下文. 在不同的地方出现, 其指向却不同.

全局或普通的函数调用

this指向全局对象, 在浏览器中就是window

console.log(this); // 全局代码, 输出window

function foo(){
  console.log(this);
}
foo(); // 简单调用, 输出window

对象的方法调用

this指向调用该方法的对象

var obj = {name:'test'};

function foo(){
  console.log(this);
}

obj.foo = foo;
obj.foo(); // 输出obj {name: 'test', foo: [Function]}

构造方法

this指向新构造的对象, 但是实例化对象的时候不能省略new, 否则仍指向全局对象

function C(){
  this.name = 'test';
  this.age = 18;
  console.log(this);
}

var c = new C(); // 输出 c
console.log(c);  // 输出 c
var d = C(); // 输出 window

通过call, apply调用

this指向方法调用时传入的第一个参数.

call方法在调用函数或方法时指定this和参数值.

apply和call语法几乎完全相同, 唯一的区别是, 参数形式不一样, apply接受一个包含多个参数的数组或类数组对象.

var obj = {name:'test'};

function foo(){
  console.log(this);
}


foo.call(obj);  // 输出obj
foo.apply(obj); // 输出obj

闭包中使用this

通常我们理解this对象是运行时基于函数绑定的, 全局函数中this对象就是window对象, 而当函数作为对象中的一个方法调用时, this等于这个对象. 由于匿名函数的作用域是全局性的, 因此闭包的this通常指向全局对象window:

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        return function(){
            return this.scope;
        }
    }
}

调用object.getScope()()返回值为global而不是我们预期的local, 在闭包中函数作为某个对象的方法调用时, 要特别注意, 该方法内部匿名函数的this指向的是全局变量. 这属于 JavaScript 的设计缺陷, 正确的设计方式是内部函数的 this 应该绑定到其外层函数对应的对象上, 为了规避这一设计缺陷, 只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可, 约定俗成, 该变量一般被命名为 that:

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        var that = this;
        return function(){
            return that.scope;
        }
    }
}
object.getScope()()返回值为local。

底层原理

在ECMA规范中详细介绍了this的实现细节.

函数对象有一个叫[[Call]]内部方法, 函数的执行其实是通过[[Call]]方法来执行的. [[Call]]方法接收两个参数thisArgargumentList, thisArg和this的指向有直接关系, argumentList为函数的实参列表.

thisArg又是怎么来的呢?我们可以和前面讨论的四种情况对应起来:

  • 普通方法调用, thisArg为undefined.
  • 通过对象调用, thisArg指向该对象.
  • 在构造方法中, thisArg为新构造的对象.
  • 通过call或apply调用, thisArg即为第一个参数.

thisArg和this的关系

规范里的描述是这样的:

If the function code is strict code, set the ThisBinding to thisArg.

Else if thisArg is null or undefined, set the ThisBinding to the global object.

Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).

Else set the ThisBinding to thisArg.

翻译过来就是:

在严格模式下, thisArg和this是一一对应的.

function foo(){
  'use strict';
  console.log(this);
}
foo(); // 输出undefined

如果thisArg为null或者undefined, this指向全局对象.

function foo(){
  console.log(this);
}
foo.call(null); // 输出window

如果thisArg为非对象类型, 则会强制转型成对象类型.

function foo(){
  console.log(this);
}
var aa = 2;
console.log(aa);  // 输出2
foo.call(aa);     // 输出 Number, 将原始值转换成了对象的包装类型

其他的情况下, thisArg和this为一一对应的关系.

确保this的指向

在实际使用this的过程中, 遇到得最多得一个问题可能就是上下文丢失的问题了. 因为javascript中的函数是可以作为参数传递的, 那么其他对象在执行回调函数时就可能造成回调函数原来的上下文丢失, 也就是this的指向改变了.

var C = function(){
  this.name = 'test';
  this.greet = function(){
    console.log('Hello,I am ' + this.name + '!');
  };
}

var obj = new C();

obj.greet(); // 输出 Hello, I am test!

setTimeout(obj.greet, 1000); // 输出 Hello,I am !

可见第二条输出中this的值改变了.

bind方法

bind方法会创建一个新函数, 称为绑定函数, 当调用这个绑定函数, 绑定函数会以创建它时传入的第一个参数作为this.

setTimeout(obj.greet.bind(obj), 1000); // 输出 Hello,I am test!

es6箭头函数

es6里面提供了一个新的语法糖, 箭头函数. 箭头函数的this不再变幻莫测,它永远指向函数定义时的this值.

var C = function(){
  this.name = 'test';
  this.greet = () => {
    console.log('Hello,I am '+ this.name + '!');
  };
}

var obj = new C();

obj.greet();                 // 输出 Hello,I am test!

setTimeout(obj.greet, 1000); // 输出 Hello,I am test!

References