What is 'this' in JavaScript

June 23, 2018 by Sylvenas

JavaScript里有个太常见的this关键字,不过却有很多的开发人员弄不懂this关键字在不同的环境中的指向,也弄不清楚应该怎样使用这个关键字。

当你彻底理解了闭包this的时候,你也就弄明白了JavaScript的核心精髓了。

在理解this的绑定之前,首先要理解调用位置:调用位置就是函数在代码中的调用位置(此处绝不是说函数的声明位置),这个过程中最重要是分析函数的调用栈(就是为了到达当前调用位置所调用的所有的函数)。

我们首先来看看关于this两种常见的误解:

指向自身

很多人认为this是指向的函数自身!为什么要从函数的内部引用函数自身呢?最常见的用处是递归。看看下面的代码:

function foo(num){
  console.log('foo: '+ num);
  this.count++;
}
foo.count = 0;

foo(1); // foo: 1
foo(2); // foo: 2
foo(3); // foo: 3

console.log(foo.count) // 0 .. why?

上面的代码显然foo函数执行了三次,但是foo.count的结果还是0,那么说明函数内的this指向函数本身这种观点显然是错误的。

那么我们代码内的count,到底是谁的count呢?实际上我们是隐式的创建了一个全局变量count,初始化的值是undefined,执行++操作的时候就会变成NaN;

它的作用域

第二种常见的误解是,this指向函数的作用域,这个问题有点复杂,因为在有些情况下它是正确的,而在有些情况下它是错误的。

在JavaScript内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JavaScript代码访问,它仅仅存在于JavaScript内部。

思考一下下面的代码,它试图(但是没有成功)跨越边界,使用this来隐式引用函数的词法作用域。

function foo(){
  var a = 2;
  this.bar();
}
function bar(){
  console.log(this.a);
}

foo(); //referenceError:a is not defined

这段代码的错误不止一个,虽然这段代码看起来好像是我们故意写出来的例子,这段代码非常完美的展示了this多么容易误导人。

首先这段代码试图通过this.bar来引用bar()函数。这样调用能成功纯属意外,我们之后会解释原因。调用bar函数最自然的方式是声落前面的this,直接使用词法引用标识符。

此外,这段代码还试图使用this,来联通foo和bar的作用域,从而让bar可以访问到foo作用域中的变量a,这是不可能实现的,使用this,不可能在词法作用域中查到什么

this到底是什么

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置毫无关系,只取决于函数的调用方式。

当一个函数被调用的时候,会创建一个活动记录(也叫执行上下文),这个记录会包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。this就是这个记录的一个属性,它会在函数执行的过程中用到

this的运行机制不是那么的容易让人理解,下面,我会尽量解释清楚不同环境下的this,首先我们从global environment开始(确保你已经安装了node,然后打开node command)。

'this' in Global Environment (默认绑定)

在全局环境下,this就完全等于一个叫global的全局对象

> this === global
//true

但是上面的代码,只有在node command中成立。如果我们尝试在一个JS文件中运行上面的代码,则会返回false,为了测试这个代码,创建一个index.js文件,然后里面的代码写:

console.log(this === global)

然后使用node command运行这个文件

$ node index.js
false

因为在一个js文件中,this是完全等于module.exports的,而不是global

'this' inside Functions (默认绑定)

函数内部的this一般是由函数的调用者来决定的,所以在函数每次执行的时候,其内部的this可能都不一样。

index.js文件中写一个简单的函数,来检查一下函数内部的this是不是等于global

function test(){
    console.log(this === global);
}
test();

如果我们用node执行上面的代码,我们将会看到打印出来true,但是如果我们在index.js的头部添加use strict,然后再次运行index.js,这个时候,会打印出来false,因为这个时候,函数内部的thisundefined

那么为什么test在非严格模式下调用的时候,内部的this,是global呢?因为这其中发生了默认绑定,什么情况下会发生默认绑定呢?就是test()是直接使用不带任何修饰符的函数引用进行调用的,因此只能使用默认绑定到全局global对象上。

为了更加仔细的了解这一点,我们看另外一个例子,我们有一个函数用来创建超级英雄的真名和绰号

function Hero(heroName,realName){
    this.realName = realName;
    this.heroName = heroName;
}

const superman = Hero('Superman','Clart');
console.log(superman)

上面的代码不是在严格模式(use strict)下运行的,在node下运行这段代码,将会打印出来undefined,而不是我们期望的SupermanClart

其中的原因就是我们这段代码不是在严格模式下运行的,函数内的this就是global对象

但是如果我们在严格模式下运行这段代码,我们将会得到一个错误,因为JavaScript不允许给undefined添加属性(这可以帮助我们,避免创建全局变量)。

最后,函数的首字母大些,意味着这个函数是一个构造函数,我们应该用一个new操作符来调用函数。替换最后两行代码如下:

const superman = new Hero('Superman','Clart');
console.log(superman)

这个时候再次运行index.js,就可以得到我们预期的结果

'this' inside constructors (new 绑定)

JavaScript本身根本没有特殊的constructor函数,我们所做不过是使用new操作符来替换函数调用。

当我们使用new操作符,调用一个构造函数的时候,实际上就是创建一个新的对象,并把函数内部的this赋值为这个对象,然后这个对象会被函数隐式的返回(除非有另外一个对象或者函数被显式的返回)。

Hero函数的最后添加代码:

return {
    heroName:'Batman',
    realname:'Bruce Wayne'
}

我们再次使用node command 运行index.js,我们会发现得到的结果superman被替换成了{heroName:'Batman',realname:'Bruce Wayne'}。但是如果我们显式的return任何非对象类型和非函数类型的数据,则最后的结果不会被显式的替换掉。

'this' in Method (隐式绑定)

当一个函数作为对象的属性方法来调用的时候,函数内部的this就指向对象本身。

看个例子,在对象hero中有一个方法dialogue,dialoguethis就是指向hero自身,hero对象会被认为是方法dialogue的调用者。

const hero = {
    heroName:"Batman",
    dialogue(){
        console.log(`I am ${this.heroName}`)
    }
}

hero.dialogue();

这是一个简单的例子,但是在真实的世界中,有时候我们很难确定方法的调用者,看看如下的代码:

const saying = hero.dialogue;
saying();

在这段代码中,把hero.dialogue方法赋值给另一个变量saying,然后把这个变量当作一个方法来调用,在node中运行这段代码,就会发现这个时候this.heroName会变成undefined,这是因为这个时候方法丢失了它的调用者,这种情况下的this会指向global,而不是hero

方法的调用者丢失的情况经常发生在,我们把一个方法作为callback传递给定外一个的时候,这个时候我们可以利用闭包,或者通过bind到我们想要的对象。

call and apply (强制绑定)

通常情况下函数的this都是隐式的被设定的,但是我们也可以显式的使用callapply方法来设置方法内部的this。 看看下面的代码:

function dialogue(){
    console.log(`I am ${this.heroName}`)
}

const hero = { heroName:'Batman' };

如果我想把hero对象作为dialogue函数的调用者,我们可以这么做:

dialogue.call(hero);
// or
dialogue.apply(hero);

如果你在严格模式之外使用callapply,并且传入的参数是null或者undefined,这个时候,nullundefined会被JavaScript engine 忽落掉。所以建议大家在严格模式下写代码。

bind (强制绑定)

当我们把一个方法作为一个callback,传递给另一个函数的时候,经常会发生丢失this,或者this指向不对的情况。

这个时候bind函数就会登场了,bind函数会创建一个新的函数,并且新的函数内部的this指向bind的参数。

const hero = {
    heroName:"Batman",
    dialogue(){
        console.log(`I am ${this.heroName}`)
    }
}

setTimeOut(hero.dialogue.bind(hero),1000);

但是有一点需要注意,通过bind生成的新的函数,内部的this是固定的,无法通过call或者apply进行修改。

Catching 'this' inside an Arrow Function (lexical scope)

箭头函数中this和其他JavaScript中函数大不相同,箭头函数本身没有属于自己的this,而是获取定义的时候的上下文作为自己的this

箭头函数在定义的时候,已经确定了this的指向,使用call和apply,也无法改变箭头函数内部的this

为了演示箭头函数内的this工作原理,我们看一下下面的例子:

const batman = this;
const burce = () => {
    console.log(this === batman);
}

burce();

我们先把this赋值给一个变量batman,然后在箭头函数内部比较函数内的thisbatman,我们发现两者完全相同 。

箭头函数内部的this无法被显式的设置,同样的箭头函数会忽略来自callapplybind传递的第一个参数,箭头函数内部的this始终指向创建时,所在的上下文环境。

箭头函数无法作为构造函数来使用,也是因为我们无法重新分配函数内部的this

那么箭头函数内部的this到底有什么用处呢?

箭头函数可以帮助我们在callback中访问到正确的this,看下面的这个例子:

const counter = {
    count:0,
    increase(){
        setInterval(function(){
            console.log(++this.count)
        },1000)
    }
}

counter.increase();

通过node index.js来执行上面的代码,我们只会得到NaN,这是因为this.count并不是指向的counter对象内部的count属性,而是指向的global.count。那么结果不言而喻。

现在使用箭头函数改写我们的代码:

const counter = {
    count:0,
    increase(){
        setInterval(() => {
            console.log(++this.count)
        },1000)
    }
}

counter.increase();

现在的情况是箭头函数在定义的时候,自动捕获了increase函数内的this,也就是counter对象,这个时候记事起就能正常工作了。

'this' in Classes (new 绑定)

class现在是JavaScript中非常重要的一员了,下面看看class中的this是怎么工作的。

每一个class都包含一个constructor,在该构造函数内的this指向的是新创建的对象。

和对象属性上的方法一样,class内方法的this也可以指定为其他的value,同样的,有时候也会丢失this

我们使用class来重新创建上面的Hero

class Hero {
    constructor(heroName){
        this.heroName = heroName;
    }
    dialogue(){
        console.log(`I am ${this.heroName}`)
    }
}

const batman = new Hero('Batman');
batman.dialogue();

constructor内的this就等于新创建的实例batman,当我们调用batman.dialogue()的时候,batman是作为dialogue方法的调用者。

不过当我们把batman.dialogue赋值给另外一个变量的时候,然后把这个变量作为一个函数来调用,毫无疑问,我们将会再次丢失函数内部的this,这个时候方法内的this,实际上指向的是undefined

const say = batman.dialogue;
say();

为什么会这样呢?因为class内的代码是隐式的在严格模式下执行的。我们直接调用了say(),而没有绑定任何this,为了解决这个问题,可以采用bind方法,来绑定函数内部的this。

const say = batman.dialogue.bind(batman);
say();

也可以在classconstructor中进行预先绑定好:

constructor(heroName){
    this.heroName = heroName;
    this.dialogue = this.dialogue.bind(this);
}

此处可以联想到 React Class Component

solution

上面讲解了各种情况下的this的绑定机制,那么到底如何确定一个函数内部的this的指向呢?

  • 函数是否在new操作符中调用(new 绑定),如果是:this绑定的就是新创建的对象;var bar = new Foo()
  • 函数是否通过call,apply(显式绑定)调用?如果是:this绑定的是制定的对象; var bar = foo.call(obj)
  • 函数是否在某个上下文对象中调用(隐式绑定)?如果是:this绑定的就是当前的上下文对象:var bar = obj.foo()
  • 如果都不是的话,那么就会使用默认绑定,也就是全局对象global;如果是在严格模式下,就是绑定到undefined; var bar = foo()