1

什么是this

JavaScript中的this是什么?
定义:this是包含它的函数作为方法==被调用时所属的对象。==

function fn1(){
    this.name = "halo";
}
fn1();
  • 我们将定义拆分一下

    • 包含它的函数:包含this的函数是fn1。
    • 作为方法被调用:fn1(); 此处fn1函数被调用。
    • 所属的对象:函数式调用函数默认所属的对象是window。

通过上面三点分析,很容易知道fn1函数里的this指向的是window。
那么如果是更复杂的场景我们如何判断this的指向呢?


this到底指向谁

如果想用一句话总结this的指向,稍微了解一些this指向的人都能脱口而出

谁调用它,this就指向谁。

也就是说this的指向是在调用时确定的,而不是在定义时确定的。这么说没有错,但是并不全面。
其实,调用函数会创建新的术语函数自身的==执行上下文==。执行上下文的调用创建阶段会决定this的指向。所以更加准确的总结应该是:

this的指向,是在调用函数时根据执行上下文所动态确定的。

在es6箭头函数之前,想要判断一个函数内部this指向谁,就根据以下四种方式来决定的。

  1. 函数式调用
  2. 上下文对象调用
  3. 构造函数调用
  4. bind、call、apply改变this指向

1、函数式调用

先来看一种相对简单的情况,函数在全局环境中被直接调用,严格模式下函数内this指向undefined,非严格模式下函数内this指向window。如下

function fn1() {
    console.log(this)
}

function fn2() {
    'use strict'
    console.log(this)
}

fn1() // window
fn2() // undefined

再看下面例子:

const age = 18;
const p = {
     age:15,
     say:function(){
          console.log(this)
         console.log(this.age)
     }
}
var s1 = p.say
s1()       

这里say方法内的this仍然指向window,因为p中的say函数赋值给s1后,s1的执行仍然是在window的全局环境中。因此上面的代码最后输出windowundefined

这里可能有人会有疑问,如果是在全局环境中,那this.age不是应该输出18么?这是因为使用const声明的变量不会挂载到window全局对象上,因此this指向window时找不到window上的age。换成var声明即可输出18.

如果想让代码输出p中的age,并且让say函数中的this指向对象p。只需要改变函数的调用方式,如下

const age = 18;
const p = {
    age:15,
    say:function(){
        console.log(this)
        console.log(this.age)
    }
}
p.say()

输出

    {age: 15, say: ƒ}
    15

因为此刻say函数内部的this指向的是最后调用它的对象。再次验证了那句话,==this的指向,是在调用函数时根据执行上下文所动态确定的。==

2、上下文对象调用

const p = {
    age: 18,
    fn: function() {
        return this
    }
}

console.log(p.fn() === p)

输出

true

如果第一节的函数式调用理解了。那么这里应该也不会有疑问。
我们再重复一遍==this的指向,是在调用函数时根据执行上下文所动态确定的。==
记住这句话后遇到更复杂的场景也可以很容易的确定this指向。
如下:

const p = {
    age: 20,
    child: {
        age: 18,
        fn: function() {
            return this.age
        }
    }
}
console.log(p.child.fn())

不论浅套关系如何变化,this都只想最后调用它的对象,因此输出18。
再升级下代码:

const o1 = {
    text: 'o1',
    fn: function() {
        return this.text
    }
}
const o2 = {
    text: 'o2',
    fn: function() {
        return o1.fn()
    }
}
const o3 = {
    text: 'o3',
    fn: function() {
        var fn = o1.fn
        return fn()
    }
}

console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())

输出结果

o1
o1
undefined
  • 第一个,应该没有问题,直接找到调用this的那个函数。
  • 第二个,看似调用o2.fn(),其实内部调用的是o1.fn(),因此还是输出o1
  • 第三个,赋值后调用fn(),相当于在全局环境调用函数。this指向window

如果现在的需求是想让

console.log(o2.fn())

输出o2,代码该如何修改?
如下:

const o1 = {
    text: 'o1',
    fn: function() {
        return this.text
    }
}
const o2 = {
    text: 'o2',
    fn: o1.fn
}

console.log(o2.fn())

3、构造函数调用

function Foo() {
    this.age = 18
}
const instance = new Foo()
console.log(instance.age)

输出18。知道输出结果并不难,但是new操作符调用构造函数时都做了什么呢?

  • 创建一个新对象;
  • 将构造函数的this指向这个新对象;
  • 为新对象添加属性、方法;
  • 返回新对象。

需要注意的是,如果在构造函数中出现显式的return,那么就要分为两种场景分析。

function Foo(){
    this.age = 18
    const o = {}
    return o
}
const instance = new Foo()
console.log(instance.age)

将会输出undefined,因为如果在构造函数中出现显式的return,并且返回一个对象时,那么创建的构造函数实例就是return返回的对象,这里instance就是返回的空对象o

function Foo(){
    this.age = 18
    return 1
}
const instance = new Foo()
console.log(instance.age)

将会输出18,因为如果构造函数中出现显式return,但是返回一个非对象的值时,那么this还是指向实例。

总结:当构造函数显式返回一个值,并且返回的是一个对象,那么this就指向这个返回的对象。如果返回的不是一个对象,this仍然指向实例。

4、bind、call、apply改变this指向

关于基础用法,这里不再赘述。需要知道的是bind/call/apply三者都是改变函数this指向的,call/apply是改变的同时直接进行函数调用,而bind只是改变this指向,并且返回一个新的函数,不会调用函数。callapply的区别就是参数格式不同。详见如下代码:

const target = {}
fn.call(target, 'arg1', 'arg2')

上述代码等同于如下代码

const target = {}
fn.apply(target, ['arg1', 'arg2'])

可以看出知识调用的参数形式不同而已,改写成bind如下所示

const target = {}
fn.bind(target, 'arg1', 'arg2')()

不光要调用bind传入参数,还是在调用bind后再次执行函数。
明白call/apply/bind的使用后,再来看一段代码:

const foo = {
    age: 18,
    showAge: function() {
        console.log(this.age)
    }
}
const target = {
    age: 22
}
console.log(foo.showAge.call(target))

结果输出22,只要掌握了call/apply/bind的基本用法,对于输出结果并不难理解。我们往往会遇到多种方式同时出现的情况,我们在说完箭头函数的this后会再详细说明this优先级相关内容。

5、箭头函数this指向

熟悉es6的人应该会知道箭头函数中的this指向,不再遵从上述的规制,而是根据外层的上下文来决定。
es5代码:

const foo = {  
    fn: function () {  
        setTimeout(function() {  
            console.log(this)
        })
    }  
}  
foo.fn()  // Window{……}

this出现在setTimeout()中的匿名函数里时,this指向window对象。这种特性势必会给我们的开发带来一些坑,es6的箭头函数就很好的解决了这个问题。
es6代码:

const foo = {  
    fn: function () {  
        setTimeout(() => {  
            console.log(this)
        })
    }  
} 
foo.fn() // {fn: ƒ}

箭头函数中的this指向,不再适用上面的标准,而是找到外层上下文,这段代码中this在箭头函数中,则找到外层的上下文的调用对象——foo。因此这里的this指向的就是foo

==注意==:当箭头函数改变了this指向后,那么该this指向就不再受任何影响,也就是说不会再次发生改变,具体在this优先级章节中会举例说明。

总结:

  • 通过call、apply、bind、new等改成this指向的操作称为显式绑定;
  • 根据上下文关系确定的this指向成为隐式绑定。

如果一段代码中即出现显式绑定又有隐式绑定,该如何确定this指向呢?
往下看

6、this优先级

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

const o1 = {
    age: 1,
    foo: foo
}

const o2 = {
    age: 2,
    foo: foo
}

o1.foo.call(o2)
o2.foo.call(o1)

如果隐式绑定优先级高于显式绑定,那么应该输出1,2。但是运行代码发现结果输出2,1。这也就说明了显式绑定中的call、apply优先级更高。
再看:

function foo (age) {
    this.age = age
}

const o1 = {}

var fn = foo.bind(o1)
fn(18)
console.log(o1.age) // 18

var f1 = new fn(22)
console.log(f1.age); // 22

分析下上面代码,fnfoo函数调用bind方法返回的函数,也就相当于是返回foo函数,并且将this指向o1对象。执行了fn(18)o1对象的age值就是18了,所以第一个输出结果是18。
然后通过new调用fn函数,这时fn函数作为构造函数被调用,this就会指向返回的实例,从而与o1对象解绑。

因此得出结论:new的优先级高于bind

还记得上一节提到的箭头函数特行么?箭头函数影响的this指向无法被修改。看下面代码:

function foo() {
    return () => {
        console.log(this.age)
    };
}

const o1 = {
    age: 2
}

const o2 = {
    age: 3
}

const fn = foo.call(o1)
console.log(fn.call(o2))

输出为2,foothis指向了o1fn接收的箭头函数的this自然也会指向o1。==而箭头函数的this是不会再次改变的==,所以尽管用显式绑定call去改变this指向,也是不起作用的。


结束啦!
this涉及知识点繁多,碰到优先级问题也是让人头疼。
没有什么捷径,唯有“死记硬背”+“慢慢理解”


巴斯光年
274 声望23 粉丝