this是Javascript语言的一个关键字,它代表函数运行时自动生成的一个内部对象,只能在函数内部使用。
this是在函数运行时绑定的,而不是编写时。它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
随着函数使用场合的不同,this的值会发生变化。但是有一个总的原则,那就是:
this指的是,调用函数的那个对象!
this指的是,调用函数的那个对象!
this指的是,调用函数的那个对象!
所以首先必须要搞清楚在JS里面有几种函数的调用方式:
- 普通函数调用
- 作为对象方法来调用
- 作为构造函数来调用
- 使用apply/call方法来调用
- Function.prototype.bind方法
接下来就按每种情况分别讨论一下this的指向问题:
调用规则
普通函数调用
var x = 1;
function test(){
alert(this.x);
}
test(); // 1
这里定义的全局变量x其实是全局对象window的一个属性,因此在调用函数时输出了全局对象的x属性值,即1。
var x = 1;
function test(){
this.x = 0;
}
test();
alert(x); //0
可以证明这里的this指代的就是全局对象window。
当然如果在严格模式中执行,那么这里的this会是undefined。
作为对象方法来调用(隐式绑定)
作为一个对象的方法进行调用时,此时this指代的就是这个对象。
var name="XL";
var person={
name:"xl",
showName:function(){
console.log(this.name);
}
}
person.showName(); //xl
var showNameA=person.showName;
showNameA(); //XL
person.showName()
,此时调用函数的很明显是person对象,因此this就是person对象。
showNameA()
则是普通函数调用,this为全局变量window。(虽然函数是person对象定义的,但此处只是普通函数调用)
var personA={
name:"xl",
showName:function(){
console.log(this.name);
}
}
var personB={
name:"XL",
sayName:personA.showName
}
personB.sayName(); //XL
虽然showName这个方法是在personA对象中定义的,但却是personB调用的,因此this还是指向personB对象。
即,对象属性引用链中只有最后一个对象会影响this;
function foo(){
console.log(this.a);
}
var obj2 = {
a:42,
foo:foo
}
var obj1 = {
a:2,
obj2:obj2
}
obj1.obj2.foo(); //42
即最后是obj2调用的函数,因此this指向obj2。
但是在一些调用情况下,this没有绑定到对象上,而是默认绑定到了全局对象或undefined(严格模式)上。这称为绑定丢失。
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo;
var a = "oops,global";
bar(); //"oops,global"
虽然bar是obj.foo的一个引用,但是实际上bar只是引用foo函数本身,与直接的函数调用没有任何区别,因此this指向的是全局变量。
另外一种情况发生在传入回调函数时:
function foo(){
console.log(a);
}
function doFun(fn){
fn();
}
var obj = {
a:2,
foo:foo
}
var a = "oops,global";
doFun(obj.foo); //"oops,global"
fn()其实引用的是foo(),因此是普通函数调用,this指向全局变量。
使用js内置的函数也没有区别:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var a = "oops,global";
setTimeout(obj.foo,100); //"oops,global"
作为构造函数调用
所谓构造函数,就是通过这个函数生成一个新对象(object)。这时,this就指这个新对象。
function test(){
this.x = 1;
}
var o = new test();
alert(o.x); // 1
function Person(name){
this.name=name;
}
var personA=Person("xl");
console.log(personA.name); // 输出 undefined
console.log(window.name);//输出 xl
在这个例子中,没有使用new操作符,因此不是作为构造函数而是普通函数调用,因此this指向全局对象window。
call/apply方法的调用(显式绑定)
call()/apply()是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数是一个对象,它们会把这个对象绑定到this,借着在调用函数时指定这个this。
通过这个方法可以直接指定对应函数this的绑定对象,因此为显式绑定。
var name="XL";
var Person={
name:"xl",
showName:function(){
console.log(this.name);
}
}
Person.showName.call(); //XL
这里call函数的第一个参数为空,默认指向window对象。
若将代码改为:
var o = {
name:"this is o"
}
Person.showName.call(o); //this is o
此时调用函数的对象是o,因此this为o。
硬绑定
使用硬绑定可以解决之前说的绑定丢失问题:
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
var bar = function(){
foo.call(obj);
}
bar(); //2
setTimeout(bar,100); //2
//硬绑定的bar不可能再修改它的this
bar.call(window); //2
在函数bar内部,我们强制把foo的this绑定到obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。
这种绑定是一种显式的强制绑定,因此为硬绑定。
由于硬绑定是一种非常常用的模式,因此ES5中提供了内置的方法bind()。
bind()方法
var name="XL";
function Person(name){
this.name=name;
this.sayName=function(){
setTimeout(function(){
console.log("my name is "+this.name);
},50)
}
}
var person=new Person("xl");
person.sayName(); //my name is XL
这里虽然是作为person对象的方法调用的,但此方法内是一个setTimeout函数。
setTimeout/setInterval/匿名函数执行的时候,this默认指向window对象,除非手动改变this的指向。
因此这里实际上函数执行时this指向的是window对象。
那么如何能绑定person对象呢?答案就是利用bind()函数
var name="XL";
function Person(name){
this.name=name;
this.sayName=function(){
setTimeout(function(){
console.log("my name is "+this.name);
}.bind(this),50) //注意这个地方使用的bind()方法,绑定setTimeout里面的匿名函数的this一直指向Person对象
}
}
var person=new Person("xl");
person.sayName(); //输出 “my name is xl”;
匿名函数使用bind( )后将this绑定为person对象。
优先级
我们已经了解了this绑定的一些规则。但是如果在某个调用位置可以应用多个规则怎么办?这时就必须给这些规则设定优先级。
- 首先,普通函数调用优先级是最低的
- 那么,显式绑定与隐式绑定呢?
function foo(){
console.log(this.a);
}
var obj1 = {
a:2,
foo:foo
}
var obj2 = {
a:3,
foo:foo
}
obj1.foo(); //2
obj2.foo(); //3
obj1.foo.call(obj2); //3
obj2.foo.call(obj1); //2
可以看到显式绑定优先级更高。
- 那么构造函数调用和隐式绑定呢?
function foo(something){
this.a = something;
}
var obj1 = {
foo:foo
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a); //2
obj1.foo.call(obj2,3);
console.log(obj2.a); //3
var bar = new obj1.foo(4);
console.log(obj1.a); //2
console.log(bar.a); //4
可以看出构造函数绑定比隐式绑定优先级高。
根据以上优先级和规则可以按照下面的顺序进行判断:
- 函数是否在new中调用?(调用构造函数)如果是的话this绑定的是新创建的对象。
var bar = new foo();
- 函数是否通过call/apply(显式绑定)或者bind(硬绑定)进行调用?如果是的话this绑定的是指定的对象。
var bar = foo.call(obj2);
- 函数是否在某个对象中调用(隐式绑定)?如果是的话this绑定的是该对象。
var bar = obj1.foo();
- 如果以上都不是的话,函数绑定到全局对象(非严格模式)或undefined(严格模式)。
var bar = foo();
绑定例外
在某些场景下this的绑定行为可能会出乎意料。
被忽略的this
如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际绑定的会是全局对象。
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null); //2
那什么情况下会传入null呢?
一种非常常见的做法是使用apply()来“展开”一个数组,并当做参数传入一个函数。
function foo(a,b){
console.log(a+","+b)
}
//把数组展开成参数
foo.apply(null,[2,3]); //2,3
apply方法需要传入一个参数当做this的绑定对象,如果不关心this的话,你仍然需要传入一个占位值,这时null可能是一个不错的选择。
间接引用
另一个需要注意的是,你有可能无意的创建一个函数的间接引用,在这种情况下this会指向全局对象。
间接引用在赋值时最容易发生。
function foo(){
console.log(this.a);
}
var a = 2;
var o = {
a:3,
foo:foo
}
var p = {a:4}
o.foo(); //3
(p.foo = o.foo)(); //2
赋值表达式p.foo = o.foo
的返回值是目标函数的引用,因此调用的是foo()而不是p.foo()或o.foo()。
箭头函数
之前介绍的绑定规则中可以包含所有正常的函数,但是ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用function关键字定义的,而是使用操作符=>定义的。
箭头函数不遵循上面的this绑定规则,而是根据外层作用域来决定this。
即,箭头函数会继承外层函数调用的this绑定。
function foo(){
//返回一个箭头函数
return (a) => {
//this继承自foo()
console.log(this.a);
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call(obj1);
bar.call(obj2); //2,不是3!
foo()内部的箭头函数会捕获调用时foo()的this。由于foo()的this绑定的是obj1,因此bar(引用箭头函数)的this也绑定到obj1。箭头函数的绑定无法被修改。
箭头函数最常应用于回调函数中。
function foo(){
setTimeout(()=>{
//这里的this继承自foo()
console.log(this.a);
},100)
}
var obj = {a:2}
foo.call(obj); //2