今天看啥  ›  专栏  ›  arzh

完整梳理this指向,看完保证秒懂

arzh  · 掘金  ·  · 2019-06-27 14:52
阅读 71

完整梳理this指向,看完保证秒懂

前言

this作为JS语言中的关键字,其复杂的指向往往是初学者混淆的地方,同时也是面试官经常询问的考点之一,理解好JS中的this指向,才能算是迈入了JS这门语言的门槛,本文将梳理出this指向的不同情景,以及如何更好、更准确的判断出this指向,让面试的时候不再为this指向头疼。

理解好this指向的关键要素

在ES5中,判断this指向的关键是在于这句话,this永远指向调用它的那个对象,想要理解本句话就只有一个重要的点,是谁调用了this,只要从这个点出发,找到这个,那么就能轻车熟路的判断this的指向。

this的调用

This 被分为三种情况:全局对象、当前对象或者任意对象;判断处于哪种情况,完全取决于函数的调用方式

从这句话可以看出,this调用的指向,其实取决于函数的调用方式,也就是在运行时,哪个函数内部使用了this,那么this就指向调用此函数的对象。这样就很容易理解this指向了,看到判断this指向问题的第一反应,去找使用了this这个函数被谁调用就完事了。如,A函数中使用了this,那么就去找哪个对象调用了A函数。

麻烦就麻烦在,如何去找到哪个对象调用了使用this的函数,因为函数的调用在JS中是可以通过多种方式的。

  1. 作为普通函数调用
  2. 作为对象方法调用
  3. 使用 applycall 调用
  4. 作为构造函数调用

那么问题就变成了只要判断函数被哪个对象调用就行了。由此分析下四种不同的函数调用情况(针对ES5)

作为普通函数调用

var a = 'arzh';
function thisName() {
    console.log(this.a);
}
thisName(); //'arzh'
console.log(window);
复制代码

我们拿这个thisName函数分析一下,作为普通函数调用时,挂载在全局,可轻易判断出函数被window调用了,那么就可以判断出this指向了window,this.a就相当于window.a,因为在ES5中定义的全局变量会变成window上的属性

所以window.a === this.a === arzh;

如果把函数改成这样

var a = 'arzh';
function thisName() {
    var a = 'arzh1'
    console.log(this.a);
}
thisName(); //'arzh'
复制代码

这个也好理解,因为this始终都是指向window,那么windowa变量一直都是'arzh',因为thisName函数中的a变量不会覆盖了window上的a变量。

我们再把函数变一下

var a = 'arzh';
function thisName() {
    a = 'arzh1'
    console.log(this.a);
}
thisName(); //'arzh1'
复制代码

这个如何理解呢,同理这个时候是window调用了thisName函数,this还是始终指向window,但是thisName函数中a变量没有加var,就被默认为全局的变量,覆盖了上一次的a变量,所以this.a就变成了最后一次定义a变量的字符串,也就是'arzh1';

作为对象方法调用

作为对象方法调用,顾名思义,就是对象调用了这个函数(方法),那么根据上面的分析,谁调用了使用this的函数,this就指向谁,可知this肯定就指向了此对象。用代码看一下

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
b.fn(); //'arzh1'
复制代码

这个就很容易理解,fn作为b对象的方法调用,即b对象调用了fn函数,那么this指向了b对象,所以this.a === b.a === 'arzh1'

修改一下函数,如下:

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
window.b.fn(); //'arzh1'
复制代码

window调用了b对象,b对象调用了fn方法,本质上,调用fn方法的还是b对象,所以this一直指向b对象,也即输出了'arzh1',如果将b对象中的a属性去掉,那么理论上就应该输出undefined,因为this还是在b对象上,b中并没有a属性。代码如下:

var a = 'arzh';
var b = {
    //a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
window.b.fn(); //undefined
复制代码

我们继续往下看

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
var fn1 = b.fn; //fn并没有被调用
fn1(); //'arzh'
复制代码

这种情况也是一样的,b对象将fn方法赋值给fn1变量,那么此时fn方法并没有被任何人调用,只是单纯的赋值。执行fn1方法之后,fn方法才算真正被调用,那么此时fn1挂载在全局window上,也就是window对象去调用了fn1方法,所以fn1的指向就变成了window,也就输出了'arzh';

那么如果是这种情况呢

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function(){
        setTimeout(function() {
            console.log(this.a)
        },100)
    }
}
b.fn(); //arzh
复制代码

从上述中可以看到b.fn()里面执行了setTimeout函数,setTimeout内的this是指向了window对象,这是由于setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。熟知EventLoop的人员应该明白,setTimeout函数其实调用的是浏览器提供的其他线程,当JS主线程走完之后,会调用任务队列的函数,也即是setTimeout函数,此时setTimeout是在window上调用的,那么this自然指向了window

使用 apply 或 call 调用

applycall函数都可以用来更改this指向,具体的用法可以参考applycall.因为applycall函数在使用上,基本只有传入参数不同的区别(apply第二个参数是数组,call第二个参数不为数组),本文拿call来讲解。我们直接看代码

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
var c = {
    a : 'arzh2'
}
b.fn(); //arzh1
b.fn.call(window); //arzh
b.fn.call(c); //arzh2
复制代码

从代码可以看出,call可以直接把this指向为传入的第一个参数。我们这里只是通过结果来得出call可以改变this指向,那么问题来了,call函数是怎么改变this指向的呢,我们可以看一下call的简单实现

Function.prototype.call = function(context) {
    // 这里的this是指调用call的函数
    // b.fn.call => this就是fn,上述已经有类似的例子
    context.fn = this;
    context.fn();
    delete context.fn;
}
复制代码

看到这里,应该就能明白call是如何改变this指向了吧,也是借用了作为对象方法调用的原理,我们只要把传入call的第一个参数当成一个对象, 然后直接调用方法,那么this自然就指向了这个对象了。我们拿上述b.fn.call(window)来说明一下

  1. context就是传入的window
  2. window.fn = this = window.fn
  3. 执行context.fn => window.fn() => window.fn()

作为构造函数调用

JavaScript 中的构造函数也很特殊,构造函数,其实就是通过这个函数生成一个新对象(object),这时候的 this 就会指向这个新对象;如果不使用 new 调用,则和普通函数一样

我们知道在JS中普通函数可以直接当构造函数使用,使用new来生成新的对象

function Name(b) {
    this.b = b;
    console.log(this)
}
var o = new Name('arzh');
//Name {b: "arzh"}

function Name(b) {
    this.b = b;
    console.log(this)
}
Name('arzh')
//Window
复制代码

可以看出如果是通过new生成的对象,this是指向已经生成的对象的。

我们来分析下具体原因:这里我们引用一下冴羽大大的new实现,具体的实现过程可以看这里

function objectFactory() {
    //新创建一个对象
    var obj = new Object(),
    //获取函数参数的第一个
    Constructor = [].shift.call(arguments);
    //原型关联
    obj.__proto__ = Constructor.prototype;
    //执行第一个参数的函数
    Constructor.apply(obj, arguments);
    //返回创建成功的对象
    return obj;
};
复制代码

这里主要关注一下第四步,new内部构造对象的时候,使用了apply函数,apply函数的一个参数就是我们需要生成的对象obj,根据上述的使用 apply 或 call 调用,我们也就不难理解使用new为什么this会指向新生成对象了。

箭头函数的this指向(ES6)

es5中,this的指向可以简单的归结为一句话:this指向函数的调用者。那么在ES6中其实这句话在箭头函数下是不成立的。我们来看看这个例子

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
b.fn(); //'arzh1'
复制代码
var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : () => {
        console.log(this.a)
    }
}
b.fn(); //'arzh'
复制代码

通过这两个例子,我们可以很明显的发现,在箭头函数中的this指向,跟在普通函数中的this指向是不同的。

函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象

所以可以这么理解,在ES6this的指向是固定的,this绑定定义时所在的作用域,而不是指向运行时所在的作用域

上述例子中,箭头函数输出 'arzh' 是因为对象不构成单独的作用域,导致fn箭头函数定义时的作用域就是全局作用域

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向

结语

其实this指向不难,比较麻烦的场景,只要用心去确定哪个对象调用了函数,就能很快确定this的指向,希望看了这篇文章,对你以后判断this指向有一定的用处

参考文章

[1] 阮一峰博客es6教程
[2] 冴羽博客new模拟实现




原文地址:访问原文地址
快照地址: 访问文章快照