理解JS中的this

121 阅读5分钟

对于很多初学者来说,this就是谜之存在,这一方面源于对this的误解,另一方面是因为this和很多语法都有着千丝万缕的关系,这些语法的细微变化都有可能影响this值的绑定。因此要清晰准确的理解JS中this的指向,确实需要付出一番努力。

基本规则

this的基本规则就是:this对象是在运行时基于函数的执行环境绑定的

这是《Javascript高级程序设计》中对this对象的描述,这里有一个非常重要的概念:执行环境。js函数在执行的时候会创建执行环境,也就是说,this的值是在函数执行的时候绑定的,而不是定义的时候,这是理解this的前提。

this值的绑定

明白了this值是在函数执行的时候绑定的,接下来只需要知道执行环境就可以确定this值了。然而,执行环境的确定并没有想象的那么简单。

总的来说,this值的绑定可以分为4类:默认绑定、隐式绑定、明确绑定和构造函数绑定。

默认绑定

全局环境中,直接调用函数,this始终指向全局对象,无论是不是在严格模式下。

隐式绑定

函数环境中,this的值取决于函数被调用的方式。

如果函数被直接调用,this的值有可能是全局对象或者undefined,这取决于是否在严格模式下。在严格模式下,this将保持进入执行环境时的值,如果this没有被执行环境定义,它将保持默认值undefined。

如果函数作为对象的方法被调用时,this指向调用函数的对象。这里有一些细节需要说明,考虑以下代码:

var foo = {
    name: 'foo',
    getName: function() {
        console.log(this.name)
    }
}

var bar = {
    name: 'bar',
    foo: foo
}

foo.getName(); // 'foo'
bar.foo.getName(); // 'foo'

getName函数既是foo对象的属性也是bar对象的属性,但是this的绑定只受最靠近的成员引用的影响。

当函数和赋值表达式同时出现的时候,this的值就会发生微妙的变化。

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

var bar = {
    name: 'bar',
    foo: foo
}

bar.foo(); // 'bar'

var fun = bar.foo;
fun(); // undefined

第一次调用foo的时候,foo函数是被bar对象引用的,也就是说foo函数被调用的时候,它是作为bar对象的属性被调用的,所以,this指向的就是bar对象。 第二次调用foo的时候,foo函数是通过赋值表达式赋值给了fun变量,fun变量本质上就是对foo函数的引用,调用fun()的时候其实就等同于直接调用foo(),自然this指向的是全局对象,全局对象没有name属性,便返回了undefined。

还有另外一种情况:回调函数

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


var bar = {
    name: 'bar',
    foo: foo
}

function fun(callBack) {
    callBack();
}

fun(bar.foo); // undefined

将bar.foo作为参数传递给了fun函数,也就是将bar.foo对foo函数的引用赋值给了callBack,调用callBack就等同于直接调用foo函数,this指向全局对象,打印的结果便是undefined。

这两种情况下,虽然foo函数都是bar对象的属性,但是foo被调用的时候,bar对象并没有在它的执行环境中,这就进一步的证明了this值是基于函数的执行环境绑定的,而与其在哪里定义的没有任何关系。

明确绑定

这里的明确绑定指的是调用一个函数的时候强制将this值绑定到某个对象上。实现的方式是通过在函数上调用call或applay方法。

考虑下面的例子:

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


var bar = {
    name: 'bar',
    foo: foo
}

var obj = {
    name: 'obj'
}
bar.foo.call(obj); // 'obj'

call方法有两个参数,this要绑定的对象和可选的要传入调用函数的参数。上面的代码通过call方法将this值强行绑定到了obj对象上。

如果想让foo函数在任何时候被调用时this值都绑定到obj对象的话,可以考虑封装一个可复用的方法。

function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    }
}

var fun = bind(foo, obj);
fun(); // 'obj'

bind方法给apply调用包了一层函数并将其返回,每次调用包装函数都是间接的调用apply将foo函数的this强制绑定到obj。

apply和call的区别是apply的第二个参数是包含多个参数的数组,call则接收的是一个参数列表。

ES5实现了bind方法,调用foo.bind(obj)的时候,会返回一个和foo具有相同函数体和作用域的函数,这个新函数的this值绑定的永远都是obj对象。

构造函数绑定

使用new关键字调用函数,这个函数就是构造函数。构造函数调用的时候,经历了以下四个步骤:

  • 创建一个新对象
  • 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象

因此,构造函数调用将this值绑定到了构造函数生成的实例对象上。

实践

讨论完4种绑定方式之后,在实践中应该遵循下面的步骤判断this的值:

  1. 函数是通过new调用的,this就是实例对象。
  2. 函数是被apply、call或者bind调用,this就是被绑定的对象。
  3. 函数是作为对象的属性被调用,this就是那个对象。
  4. 函数被直接调用,this就是全局对象。或者在严格模式下,函数在局部作用域内调用,this就是undefined。

以上就是this绑定的主要内容。