JavaScript中的this详解

1,216 阅读10分钟

this是JavaScript这门语言中极其重要的一个知识点,特别是关于面向对象的相关的写法,可以说掌握了this的特性,相当于掌握了一大半JavaScript面向对象的编写能力。总的来说,JavaScript中的this大概有7种情况,理解到位了这些情况,基本上就掌握了这部分相关的内容,所有的高级写法,都是基于这些情况的演变。这7种情况分别是:

  • 全局环境调用下的this
  • 事件处理函数中的this
  • 对象方法内的this
  • 构造函数中的this
  • 原型链上函数中的this
  • getter和setter中个this
  • 箭头函数中的this
    除了最后一个箭头函数中的this指向,是基于函数书写时候确定的,其它所有情况下,JavaScript中的this,都是由调用时决定的。很多时候,大家会很纳闷,这个this,到底是什么?this可以是全局对象window,可以是一个具体的元素比如<div></div>,也可以是一个对象比如{},还可以是一个实例等等。至于this到底是什么,就看函数执行的时候,到底是谁调用了它。

1.全局环境调用

我们所说的全局环境,其实指的就是window这个对象,也就是我们在浏览器中每打开一个页面,都会生成的一个window。先来看看最简单的全局调用。

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

fn1();  // window

// 相当于
window.fn1();

我们都知道,全局下使用var声明的变量,都会隐式的被创建为window对象的属性和方法。所以,当你看到一个函数被调用而没有前缀的时候(也就是说不是通过"."符号来调用),这其实就是全局对象window在调用它。因此,此时函数内部的this是指向window对象的。再来看个变化版本。

let o = {
    name: 'abc',
    fn: function() {
        console.log( this.a );
    }
}

let fn2 = o.fn;
fn2();  // undefined

是的,虽然fn2拿到的是对象o里面的一个方法,但是,万变不离其宗,在执行fn2()的时候,仍然是没有前缀的,那是谁在调用fn2的?当然是window对象。所以这里的this也指向window。

1.1 严格模式和非严格模式的区别

我们现在知道,全局对象window调用的函数,内部的this就是指向window。但是这里有个问题需要注意一下。JavaScript有严格模式和非严格模式之分(严格模式就在代码的顶部加上一句"use strict")。在这两种情况下,this的指向是有区别的。
非严格模式下this指向我们已经讨论过了,指的是window对象,而严格模式下的全局调用,this指向的是undefined。

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

fn1();  // undefined

2.事件处理函数中的this

JavaScript中对于事件的处理是采用异步回调的方式,对一个元素绑定一个回调函数,当事件触发的时候去执行这个函数。而对于回调函数的绑定,有下面几种情况:

  • 元素标签内绑定
  • 动态绑定
  • 事件监听 这几种情况下,回调函数内的this分别又是什么呢?分别来看看。

2.1元素标签内绑定

<div id="div1" onclick="console.log( this )"></div>

点击元素div1后,我们发现控制台打印的是"<div id="div1" onclick="console.log( this )">",可以知道的是,元素内联所执行的语句中的this,指向的是元素本身。但是,有一个特例,来改动一下方式。

<div id="div1" onclick="(function () {console.log( this )}()"></div>

看明白了吗,元素内联的是一个匿名自执行函数,这个时候匿名自执行函数中的this,就不是指向元素本身了,而是window对象!虽然这种写法很无聊,但这就是内联写法我们需要注意的一个点。我们可以这样理解,匿名自执行函数有独立的作用域,相当于是window在调用它。这种情况,知道就好,无需太花力气死磕。

2.2 动态绑定

let div1 = document.getElementById("div1");

div1.onclick = function() {
    console.log( this );    // div1
}

这是通过动态绑定的方式,给元素添加了事件,这种情况下,当回调函数执行的时候,是元素div1在调用它,所以此时函数内部的this,是指向元素div1的。

2.3 事件监听

let div1 = document.getElementById("div1");

div1.addEventListener("click", function() {
    console.log( this );    // div1
    }, false);

同样的,通过事件监听器的方式绑定的回调函数,内部的this也是指向div1。所以我们可以总结一下得知:事件处理函数中的this,指向的是触发这个事件的元素。

3.对象方法中的this

在JavaScript中,对象是可以有属性和方法的,这个方法,其实就是函数。既然是函数,那么内部肯定也会有this,作为对象方法中的this,到底是指的什么呢?看个简单的例子。

var name = 'aaa';
let obj = {
    name: 'jack',
    fn: function() {
        console.log( this.name );
    }
}

let f1 = obj.fn;

obj.fn();   // jack
f1();       // aaa

作为对象的方法调用的函数,它内部的this,就指向这个对象。在这个例子中,当通过obj.fn()的形式调用fn函数的时候,它内部的this指的就是obj这个对象了。至于第二种情况,先把obj.fn赋值给f1,然后通过执行f1来执行函数的情况,我们在上面已经说过,这个时候,其实是window对象在调用f1,因此它内部的this就是指向window对象,因而打印的就是'aaa'。
如果是一个对象中嵌套着比较深的方法,它内部的this又是什么呢?

let person = {
    name: 'jack',
    eat: {
        name: 'apple',
        fn1: function() {
            console.log( this.name );
        },
        obj: {
            name: 'grape',
            fn2: function() {
                console.log( this.name );
            }
        }
    }
}

person.eat.fn1();       // apple
person.eat.obj.fn2();   // grape

这里遵守一个就近原则:如果是通过对象方法的方式调用函数,则函数内部的this指向离它最近一级的那个对象。在这个例子中,person.eat.fn1()这种调用,fn1中的this指的就是eat这个对象;person.eat.obj.fn2()这种调用方式,fn2中的this,指的就是obj这个对象。

4.构造函数中的this

构造函数其实就是普通的函数,只是它内部一般都书写了许多this,可以通过new的方式调用来生成实例,所以我们一般都用首字母大写的方式,来区分构造函数和一般的函数。构造函数,是JavaScript中书写面向对象的重要方式。

function Fn1(name) {
    this.name = name;
}

let n1 = new Fn1('abc');
n1.name; // abc

这是一个非常简单的构造函数书写方式,以及对构造函数的调用。构造函数中的this,以及new调用的这种方式,其实都是为了能够创造实例服务的,否则也就没有意义了。那么,构造函数中的this也就很清楚了:它指向构造函数所创造的实例。当通过new方法调用构造函数的时候,构造函数内部的this就指向这实例,并将相应的属性和方法"生成"给这个实例。通过这个方法,生成的实例才能够获取属性和方法。
凡事总有例外嘛,构造函数中有这样一种例外,我们看看。

function Fn1(name) {
    this.name = name;
    return null;
}

function Fn2(name) {
    this.name = name;
    return {a: '123'};
}

let f1 = new Fn1("ttt");
console.log( f1 );  // {name: "ttt"}

let f2 = new Fn2("ggg");
console.log( f2 );  // {a: "123"}

f1是通过new Fn1创建的一个实例,这没有问题。但f2为什么不是我们所想的结果呢? 当构造函数内部return的是一个对象类型的数据的时候,通过new所得到的,就是构造函数return出来的那个对象;当构造函数内部return的是基本类型数据(数字,字符串,布尔值,undefined,null),那么对于创建实例没有影响。

5.原型链函数中的this

原型链函数中个this,其实跟构造函数中的this一样,也是指向创建的那个实例。

function Fn() {
    this.name = '878978'
}

Fn.prototype.sum = function() {
    console.log(this)
    return this;
}

let f5 = new Fn();
let f6 = new Fn();

console.log( f5 === f5.sum() );     // true
console.log( f6 === f6.sum() );     // true

6.getter和setter中的this

我们知道,JavaScript中getter和setter是作为对对象属性读取和修改的一种劫持。可以分别在读取和设置对象相应属性的时候触发。

let obj = {
    n: 1,
    m: 2,
    get sum() {
        console.log(this.n, this.m);
        return '正在尝试访问sum...';
    },
    set sum(k) {
        this.m = k;
        return '正在设置obj属性sum...';
    }
}

obj.sum;   // 1,2
obj.sum = 5;  // 正在设置obj属性sum..

getter和setter中的this,规则跟作为对象方法调用时候函数内部的this指向是一样的,它指的就是这个对象本身。

7.箭头函数中的this

箭头函数是ES6中新推出的一种函数简写方法,跟ES5函数最大的区别,就要数它的this规则了。在ES5的函数中,this都是在函数调用的时候,才能确定具体的this指向。而箭头函数,其实是没有this的,但是它内部的这个所谓this,在箭头函数书写的时候,就已经绑定了(绑定父级的this),并且无法改变。看个例子。

let div1 = document.getElementById("div");

div1.onclick = function() {
    setTimeout(() => {
        console.log( this );    // div1
    }, 500);
}

我们知道,setTimeout中所绑定的回调函数,其实是window在调用它,所以它内部的this指向的是window。但是,当回调函数是箭头函数的写法的时候,内部的this竟然是div1!这在箭头函数书写的时候,就已经决定了它内部的this指向,就是它父级的this。而它父级函数作用域中的this,其实就是元素div1。作为对象方法的箭头函数,其实也是类似的道理。

var name = 'aaa';
let obj = {
    name: 'jack',
    fn1: () => {
        console.log( this.name );
    }
}

obj.fn1();  // aaa

没错,还是那句话,当我们写下箭头函数的时候,它内部的this就已经确定了,并且无法修改(call, apply, bind)。这个例子中,箭头函数最近的父级作用域显然是全局环境window,因此它的this就指向window。

8.call, apply, bind的用法

说到JavaScript中的this,就没法不说call, apply, bind这三个方法。在所有JavaScript函数的高级用法,或者是JavaScript框架中,都会有这三个方法的踪影。这三个方法都是Function.prototype上的方法,所以所有的函数都默认继承了这三个方法。现在具体说说这三个方法的分别用途。

8.1 call

call方法可以实现对函数的立即调用,并且显示的指定函数内部的this以及传参。

let obj = {
    color: 'green'
}

function Fn() {
    console.log( this.color );
}

Fn();   // undefined

Fn.call(obj);   // green

call可以实现对函数的立即调用,并且改变函数内部的this指向。上面的例子中,直接调用函数Fn的时候,它内部的this指向window对象,因此打印的是undefined;当通过call指定函数内部的this指向obj的时候,它就能获取到obj上的属性和方法了。call调用还能实现调用时候的传参,请看。

let obj = {
    color: 'blue'
}

function Fn(height, width) {
    console.log(`the tree is ${this.color}, and the tall is ${height}, width is ${width}`);
}

Fn.call(obj, 20, 3);   // the tree is blue, and the tall is 20, width is 3

8.2 apply

apply的作用和call是一模一样的,都是实现对函数内部this的改变,唯一的区别就是传参的方式不一样:call是通过一个一个参数的方式传递参数,而apply是通过数组的形式传递多个参数。

let obj = {
    color: 'orange'
}

function Fn(height, width) {
    console.log(`the tree is ${this.color}, and the tall is ${height}, width is ${width}`);
}

Fn.apply(obj, [16, 7]);   // the tree is orange, and the tall is 16, width is 7

8.3 bind

call和apply都是实现对函数的立即调用,并且改变函数内部this的指向,如果说我只想改变函数内部的this,而不执行函数,该怎么办?这个时候,就需要用到bind。

let person = {
    name: 'jack'
}

function Person() {
    console.log(this.name);
}

let p1 = Person.bind(person);
p1();   // 'jack'

当一个函数执行完bind方法后,会返回一个新的函数,而这个新的函数跟原函数相比,内部的this指向被显示的改变了。但是不会立即执行新的函数,而是在你需要的时候才去调用。 但是有一点需要注意,返回的新函数p1,它内部的this就无法再改变了。接着上面的例子。

let animal = {
    name: 'animal'
}

let p2 = p1.bind();
p2();   // 'jack'

p2的this依然是指向obj,而非animal。