js基础:this你到底指向哪里?

175 阅读5分钟

this是javascript中最常用也是比较难理解的一个关键字了,看了很多相关知识,以为理解的差不多了,然后偶然刷到一题又感觉自己不懂了。一直在这种懂与不懂之间徘徊了。
最近在看javascript基础知识,有了一些自己的总结与体会。最开始是看到下面一题:

var age = 100;
var test = {
    age: 120,
    sayAge: function(){
        console.log(this.age)
        function go(){
            console.log(this.age)
        }
        go.prototype.age = 60;
        return go
    }
}
var p = test.sayAge()  // ->120
p() // ->100
  • 如果将上面的代码最后加上一句new (test.sayAge())()
    第一步:test.sayAge() 打印120,返回go
    第二步:new go() 打印60
  • 如果将go.prototype.age = 60;这句话注释了,执行new (test.sayAge())()
    打印结果:120,undefined(this指向new出来的对象实例,不再是window,而this这个对象上没有age这个属性)

之前判断this的指向就是使用口诀:“谁调用它,它就指向谁”,但是有时候这个ta很让人困惑,最后就分不清this指向谁了;有时候又错误的理解this指向它的作用域。比如上面的第二个问题,this没有age,他的全局作用域上有age的,就会错误的认为会打印全局的age。例如下面的代码也常会错误将this=作用域;

var a = 100;
function car(){
    console.log(this.a)
}
var myCar = {
    car: car
}
myCar.car() //->undefined而不是100

在ES6之前,每个函数的this都是在调用时才被绑定,所以理解函数的调用位置很重要(不是声明位置)

在《你不知道的javascript》上卷中对this的指向总结了4条规则:

  • 默认绑定(函数在不带任何修饰情况下进行调用时,此时函数中的this在非严格模式下指向window )

    function test(){
        bar() //->bar的调用位置
    }
    function bar(){
        foo() // ->foo的调用位置
    }
    function foo(){
        console.log('end')
    }
    test() // ->test的调用位置

上面调用顺序是test->bar->foo,找到了函数的调用位置,我们来分析它的this绑定,上面的三个函数都是作为独立函数调用的,相当于在全局环境下调用,默认的全局环境中(非严格模式下),this指向window(严格模式下this指向undefined)。

  • 隐式绑定(调用位置是否有上下文或者说被某个对象拥有,此时this指向被拥有的对象)

var a = 101;
function foo(){
    console.log(this.a)
}
var obj = {
    a: 100,
    foo: foo
}
obj.foo() // ->100

在上述代码中foo函数作为obj对象foo属性的一个引用来调用,因此可以说obj拥有foo这个函数,所以此时foo函数中的this指向的是obj这个对象。
但是对象引用链只有最后一层会影响调用位置: obj.obj2.foo()此时foo中的this只会指向obj2

在隐式绑定中最容易弄混的是下面这种情况:

var a = 1
function foo(){
    console.log(this.a)
}
var obj = {
    a:2,
    foo: foo
}
var temp = obj.foo
temp();

在上面的那段代码中有时会错误的理解为最后打印的结果是:2,实际控制台打印的是: 1
在上述代码中的倒数第二步中我们将obj.foo这个引用赋值给temp这个对象,这里相当于将对象的赋值(this是不会被复制过来了,this只有在调用的时候才会被制定)此时就temp这个对象指向foo函数,最后在调用temp()这个函数的时候,可以使用第一条默认绑定规则,此时this就指向window了.

到这里就能明白最上面的那道题中的执行结果了

var p = test.sayAge()  // ->120
p() // ->100

第一个使用隐式绑定规则this指向test,第二条中this指向window(声明在全局作用域中的变量就是全局对象的一个同名属性,不是在作用域中查找的),所以window上有age这个属性。

  • 显示绑定(使用call、apply、bind,此时this指向前面3个方法所传第一个参数值)

var a = 1
function foo(){
    console.log(this.a)
}
var obj = {
    a:2,
    foo: foo
}
var temp = obj.foo.bind(obj)
temp(); // ->2
// temp.call(obj)  ->2
// temp.apply(obj) ->2

这里显示的将temp中this强制绑定到obj这个对象上(bind函数相当于返回一个新函数,不会执行),call/apply会执行函数,两者只是所传的参数不同,在传的参数为null、undefined时this指向window。

  • new绑定

当将一个函数作为构造函数来调用时,使用new来创建一个实例时,会自动执行下面4步

var mycar = new Car()
function Car(){
    var this;//创建一个对象
    this = {
        __proto__: Car.prototype
    }
    return this
}
  • 创建一个全新的对象
  • 这个新对象默认有一个__proto__属性指向原型
  • 这个对象绑定到函数调用的this
  • 如果函数没有返回其他对象,那么返回这个新对象
function Car(color){
    this.color = color
}
var mycar = new Car('red')
mycar.color //->red

回到最初的题目

new (test.sayAge())()

执行test.sayAge()时采用隐式绑定规则,此时this指向test这个对象,所以打印120。这个执行结果会返回一个go函数,后面继续执行new go()这时就使用最后一条规则,this指向构造函数go创建的这个实例对象,虽然这个实例对象上没有age这个属性,但是他在创建的时候就从原型上继承了一个age属性。所以在他的原型上找到了,打印60。
后续的注释掉go.prototype.age = 60;这句话,这句导致创建的这个实例对象自己本身没有,原型上也没有,则只能打印undefined。

所以判断一个运行中函数的this,首先需要找到这个函数的调用位置,然后就使用上述4条规则来判断this绑定的对象。但是ES6的箭头函数不适用上述4条规则。