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);
}
此处可以联想到 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()