深入理解JavaScript中的this指向

javascript/jquery

浏览数:268

2019-3-31

与其他语言相比,js中的this有所不同,也是比较头疼的问题。在参考了一些资料后,今天,就来深入解析一下this指向问题,有不对的地方望大家指出。

为什么要用this

对于前端开发者来说,this是比较复杂的机制,那么为什么要花大量时间来学习呢,先来看一段代码。
如果不使用this,要给identify( )和speak( )显式传入一个对象:

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm" + identify(context);
    console.log(greeting);
}
identify(you);
speak(me);

可以看到,speak( )里面直接写了identify( )的函数名,然而,随着使用模式越来越复杂,显式传递的上下文会让代码变得混乱,尤其体现在面向对象中。
显然,this提供了一种方式来隐式“传递”一个对象的引用,更加简洁,易于复用。

this的误解

1. this指向函数本身

记录函数foo被调用的次数:

function foo(num) {
    console.log("foo:" + num);
    
    // 记录次数
    this.count++;
}

foo.count = 0;

var i;

for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}

// foo: 6
// foo: 7
// foo: 8
// foo: 9

//foo被调用了多少次?
console.log(foo.count); // 0

从前两次的console.log( )可以看出,foo确实被调用了4次,但是foo.count仍然为0,显然this指向函数本身的理解是错误的。

2. this指向函数作用域

要明确的是,this在任何情况下都不指向函数的词法作用域。因为,作用域“对象”无法通过JavaScript代码访问,它存在于JavaScript引擎内部。
下面的代码试图使用this来隐式引用函数的词法作用域,没有成功:

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log(this.a);
}
foo(); // ReferenceError: a is not defined

直接报出了访问不到foo( )中的a。ReferenceError和作用域判别失败相关,而TypeError代表作用域判别成功,但是对结果的操作是非法的、不合理的。

this是什么

排除了以上两个误解之后,来看一下this到底是什么。
this是运行时绑定的,它和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(执行上下文),这个记录包含函数在哪里被调用,函数的调用方式、传入的参数等,this就是这个记录的一个属性,在函数执行的过程中用到。即,this总是指向调用它所在方法的对象

1. 在浏览器中,调用方法没有明确对象时,this指向window
function foo() {
    console.log(this.a);
}

var a = 2;

foo(); // 2

在全局中声明变量a = 2,然后在全局中直接调用foo( ),this指向了全局对象,得到a的值。
要注意的是,在严格模式(strict mood)下,如果this没有被执行环境定义,那它将绑定为undefined。

function foo() {
    "use strict";
    
    console.log(this.a);
}

var a = 2;

foo(); // TypeError: this is undefined

在严格模式下,调用foo( )不影响this绑定。

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

var a = 2;

(function() {
    "use strict";
    
    foo(); // 2
})();
2. 在浏览器中,setTimeout、setInterval和匿名函数执行时的当前对象是全局对象window
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2,
    foo: foo
};

var a = "global";

setTimeout(obj.foo, 100); // "global"

JavaScript中的setTimeout( )的实现和下面伪代码相似:

function setTimeout(fn, delay) {
    // 等待delay毫秒
    fn(); // 调用函数
}
3. apply / call / bind可以强制改变this指向
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2
};

foo.call(obj); // 2
foo.apply(obj); // 2
foo.bind(obj); // 2

call和apply的区别在于第二个参数,call是把args全部列出来,用“,”分隔,而apply是一个类数组。call、apply是硬绑定,通过硬绑定的函数不能再修改它的this。

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

var obj = {
    a: 2
};

var bar = function() {
    foo.call(obj);
}

bar(); // 2
setTimeout(bar, 100); // 2

bar.call(window); // 2

函数foo( )内部手动调用了foo.call(obj),把foo的this强制绑定到了obj,所以后面即使又把bar( )绑定到了window,还是无法改变this指向。

4. new操作符改变this指向

在传统的面向对象语言中,会使用new初始化类,然而在JavaScript中new的机制和面向对象语言完全不同。在js中,构造函数只是使用new操作符时被调用的函数,它们并不属于一个类,也不会实例化一个类。也就是说,js中,不存在所谓的“构造函数”,只有对函数的“构造调用”。

function foo(a) {
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2

使用new调用foo( ),会构造一个新对象并把它绑定到foo( )调用中的this上。

优先级

既然有那么多可以改变this的指向,那么它们的优先级是怎么样的呢,记住这句话:范围越小,优先级越高。可以按照下面的顺序来判断:

  1. 判断函数是否在new中调用过:

    var bar = new foo();
  2. 判断函数是否通过call、apply、bind绑定过:

    var bar = foo.call(obj);
  3. 判断函数是否在某个上下文对象中调用过:

    var bar = obj.foo();
  4. 如果以上情况均不存在,那么在严格模式下,绑定到undefined,否则绑定到全局对象:

    var bar = foo();