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,因为这个时候,函数内部的this是undefined。
那么为什么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,而不是我们期望的Superman和Clart。
其中的原因就是我们这段代码不是在严格模式下运行的,函数内的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,dialogue的this就是指向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都是隐式的被设定的,但是我们也可以显式的使用call和apply方法来设置方法内部的this。
看看下面的代码:
function dialogue(){
    console.log(`I am ${this.heroName}`)
}
const hero = { heroName:'Batman' };
如果我想把hero对象作为dialogue函数的调用者,我们可以这么做:
dialogue.call(hero);
// or
dialogue.apply(hero);
如果你在严格模式之外使用call和apply,并且传入的参数是null或者undefined,这个时候,null和undefined会被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,然后在箭头函数内部比较函数内的this和batman,我们发现两者完全相同 。
箭头函数内部的this无法被显式的设置,同样的箭头函数会忽略来自call、apply和bind传递的第一个参数,箭头函数内部的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();
也可以在class的constructor中进行预先绑定好:
constructor(heroName){
    this.heroName = heroName;
    this.dialogue = this.dialogue.bind(this);
}
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()