this是JavaScript中又一个容易造成错乱的特性,很多人都觉得非常复杂,但实际上只需要记住4条法则就可以判断this到底指向什么。this是在函数调用时才绑定指向对象的,它指向什么完全取决于函数在哪里被调用。它没有我们日常生活中所说的「那个,是那个,就是我跟你说过的那个!」那么难懂。
一、关于this
1.1 this的作用
「自适应」,我觉得用来形容this的作用十分形象,如果你写过Java代码,一定对this不陌生——它指向调用当前函数的那个对象。但是在JavaScript中this的指向分为好几种:
let name='global';
function Foo(){
this.name = 'Foo';
this.printName = function(){
console.log(this.name);
}
}
let foo = new Foo();
let printName = foo.printName;
let a = {name:'a'},
b={name:'b'};
a.printName = foo.printName;
//同一个函数不同的输出结果
printName();//'global'
a.printName();//'a'
foo.printName();//'Foo'
(printName.bind(b))();//'b'
复制代码
同一个函数——都是来自foo.printName,this却各不一样,是不是很头疼?这里可以看成是一个大坑,然而你也可以选择了解清楚其内部的机理然后运用它,让this的「自适应」助你一臂之力,好与坏在于你的看法。
1.2 关于this的误解
大家对于this有两种普遍的理解:一个是指向函数本身,二是指向函数运行的作用域。
1.2.1 指向自身
从字面上来看,指向自身似乎是一个不错的解释,但实际上并不是这样:
function foo(){
console.log(this.name);
}
foo.name = 'foo';
foo();//undefined
console.log(foo.name);//'foo'
复制代码
foo函数是一个对象,我们给它添加了一个name属性并赋值'foo',但上面的代码告诉我们,函数中的那个this并不是指向函数本身,而是指向另一个对象。如果你想在函数内部引用函数自身,只能这样:
function foo(){
console.log(foo.name);
}
foo.name = 'foo';
foo();//'foo'
复制代码
所以,this并不是指向函数本身。
1.2.2 指向作用域
认为this指向作用域这个观点有时候成立有时候不成立。
this在任何情况下都不会指向函数的词法作用域,每个作用域都有一个与之关联的「变量对象」,作用域中定义的变量和函数都是它的属性。但是这个变量对象无法通过JavaScript代码访问到,它位于JavaScript引擎内部。 我们看一看下面这段代码:
function foo(){
var a = 1;
this.bar();
}
function bar(){
console.log(this.a);
}
foo();//undefined
复制代码
首先定义了foo函数,拥有一个变量a,然后想通过this.bar()调用foo所处的作用域中的bar函数。其次定义了一个bar函数,它想在控制台输出bar函数运行时所处的作用域中定义的变量a,在这里就是foo中的a。
从运行结果上来看,通过this.bar()调用bar函数成功了,说明这个this指向了foo函数运行时所处的作用域,而bar函数中希望使用this.a引用foo函数中的变量a,却没有成功。说明这个this并没有指向bar函数运行时的作用域。
所以,认为this指向函数运行时的词法作用域是不对的。
二、this的指向
2.1 调用位置
我们一开始提到:「this的指向取决于函数被调用的位置。」函数调用位置与函数声明位置不同,它是指函数在代码中被调用的位置。为了弄清楚函数的调用位置,我们需要理清函数的「调用栈」,即为了到达当前执行位置先后调用的所有函数顺序。请看下面的代码:
function foo(){
//当前调用栈是foo,所以当前调用位置位于全局作用域中
console.log('this is foo');
bar();//bar被调用
}
function bar(){
//当前调用栈是foo-->bar,所以当前调用位置位于foo函数中
console.log('this is bar');
baz();//baz被调用
}
function baz(){
//当前调用栈是foo-->bar-->baz,所以当前调用位置位于bar中
console.log('this is baz');
}
foo();
复制代码
以baz为例:baz运行之前首先要执行foo函数,然后foo函数里调用了bar函数,bar函数执行时又调用了baz函数,从而使baz函数运行了,这就像是一个链。而baz的执行位置就在链中的前一个——bar函数中。
2.2 绑定规则
绑定规则一共有4条,通过上述方法找到函数的调用位置后就可以应用下面四条规则判定this的指向。
2.2.1 默认绑定
默认绑定规则意为当不符合其他规则时应用的规则,默认绑定时this指向全局对象window,最常见的是「独立函数调用」。
function foo(){
console.log(this.a);
}
var a = 1;
foo();// 1
复制代码
我们知道在全局作用域中声明的变量将会归属于全局对象window的一个属性,在默认绑定中,this指向全局对象。在这里foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能运用默认绑定,无法应用其他规则。
不过如果函数内部使用了严格模式,this将无法通过默认绑定绑定到全局对象,它会绑定到undefined:
function foo(){
'use strict';
console.log(this.a);
}
var a = 1;
foo();//TypeError: Cannot read property 'a' of undefined
复制代码
此外在严格模式下还有一个重要的细节需要注意:只有函数运行在非严格模式下时,默认绑定才能绑定到全局对象,严格模式下与foo()的调用位置无关。
function foo(){
console.log(this.a);
}
var a = 1;
(function(){
'use strict';
foo();// 1
})()
复制代码
2.2.2 隐式绑定
了解何为隐式绑定之前大家首先要知道:无论在哪里声明的函数,对应的变量持有的始终是这个函数的引用,而不是函数本身。也就是说这个函数不属于任何对象,所有的对象只是持有了这个函数的引用。例如:
var obj = {
a:1,
foo:function (){
console.log(this.a);
}
}
var a = 2;
var foo = obj.foo;
foo();//2
obj.foo();//1
复制代码
在这里我们虽然是在obj对象内部定义了foo函数,但实际上无论是obj.foo还是全局作用域中的foo,都只是持有函数的引用,没有谁拥有这个函数。
foo()输出结果为2是因为执行了默认绑定规则,这一点不用再讲。而obj.foo()之所以输出结果为1,是因为这种调用形式使用了obj的上下文来引用函数,简单地说就是运行时函数的this被绑定到了obj,所以输出了obj中的变量a的值。
另外,只有对象属性链的最后一层才会影响this的绑定:
var obj1 = {
a:'obj1',
obj2:obj2
};
var obj2 = {
a:'obj2',
printA:printA
};
function printA(){
console.log(this.a);
}
obj1.obj2.printA();//'obj2'
//其实相当于
var obj = obj1.obj2;
obj.printA();
复制代码
隐式丢失 隐式丢失的根源就是我们一开头讲的对象持有函数的本质,还是那段代码:
var obj = {
a:1,
foo:function (){
console.log(this.a);
}
}
var a = 2;
var foo = obj.foo;
foo();//2——隐式丢失
obj.foo();//1
复制代码
这里foo和obj.foo所引用的函数是同一个函数,区别在于前者对于函数的调用没有任何修饰,而后者用一个对象来引用这个函数,可以用接下来讲到的显示绑定来理解:
//使用call(...)或apply(...)来绑定this
//foo()相当于↓
foo.call(undefined);
//obj.foo相当于↓
foo.call(obj);
复制代码
最常见的是在传递回调函数的时候,如果我们理解了函数与引用函数的概念,就能明白隐式丢失的原理。
var obj ={
a:2
foo:function(){
console.log(this.a);
}
};
function doFoo(callback){
callback();
}
var a = 1;
doFoo(obj.foo);//1
//传递的是函数的引用
复制代码
2.2.3 显式绑定
显式绑定就是 通过call(...)或者apply(...)来指定this绑定的对象,这两个方法第一个参数为要绑定的对象,接着是要向方法传递的参数,具体细节请自行学习。
function foo(){
console.log(this.a);
}
var obj1 = {
a:1,
};
var a = 2;
foo.call(obj1);//1
foo();//2
复制代码
可以看到,显式绑定并不会导致this不可变,只是在函数运行时更改了this的绑定。如果我们想达到始终更改函数this的效果行不行呢?也是可以的,通过再包装一层函数实现:
function foo(){
console.log(this.a);
}
var obj1 = {
a:'obj1'
};
var obj2 = {
a:'obj2'
}
function bar(){
foo.call(obj1);
}
bar.call(obj2);//'obj1
复制代码
我们在bar函数内封装了对于foo函数的显式绑定,此后通过bar来调用显式绑定后的foo,无论bar绑定什么,都不会影响内部foo的绑定。但还是一样,foo函数本身并没有什么改变。
硬绑定可以用于预设参数:
var obj ={
a:1
};
function cal(num){
console.log(this.a + num);
}
function getResult(){
cal.apply(obj,arguments);
}
复制代码
这是一个很简单的应用,通过obj预设了a的值并可以动态更改。另外我们还可以创建一个动态指定this绑定对象的函数:
function cal(num){
console.log(this.a + num);
}
function bind(fn,obj){
return function(){
fn.apply(obj,arguments);
}
}
var obj = {
a:10
};
var getResult = bind(cal,obj);
getResult(10);//20
复制代码
ES5开始提供了bind(...)方法来进行硬绑定,该方法返回一个硬编码的函数,它会把this绑定到参数并调用原始函数。
function cal(num){
console.log(this.a + num);
}
var obj = {
a:10
};
var getResult = cal.bind(obj);
getResult(20);//30
复制代码
2.2.4 new绑定
new使用在「构造函数」实例化上,但实际上JavaScript中并没有构造函数,所有的函数都是一样的,他们都可以被new操作符调用。当函数被new操作符调用时,我们称之为「构造函数调用」。发生构造函数调用时执行了以下步骤:
- 构建一个全新的对象;
- 对对象进行[[prototype]]连接;
- 将这个新的对象绑定到函数的this上;
- 如果构造函数调用没有返回其他对象,将自动返回这个创建的对象。
function Foo(a){
this.a = a;
}
var bar = new Foo(1);
console.log(bar.a);//1
复制代码
这里的输出结果完全可以用上面4个步骤来解释。
2.3 四条规则的优先级
既然有这么多条规则,就会有优先级。这四条规则的优先级为:
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
试验一下:
function foo(a){
this.a = a;
}
var obj1 = {
foo:foo
};
var obj2 = {};
obj1.foo('obj1');
console.log(obj1.a);//'obj1'
obj1.foo.call(obj2,'obj2');
console.log(obj2.a);//'obj2'
//显示绑定比隐式绑定优先级高↑
var objBind = foo.bind(obj2);
objBind('bind');
var obj3 = new ovjBind('obj3');
console.log(obj3.a);//'obj3'
//new绑定比显式绑定优先级高↑
复制代码
上面展示的硬绑定被new绑定修改了,看起来非常意外,因为我们实现的硬绑定是在外层包裹了一层函数的。实际上bind(...)更加复杂,简单地说就是当硬绑定的函数被new调用时,会被嗅探到,这时将会用新创建的this代替硬绑定的this。
弄清楚了四条规则的优先级,那么在以后判断this时就有据可循了——找到函数的调用位置,然后按照以下顺序判断:
- 是否有new调用,如果有this就是新创建的对象。
- 是否有显式绑定,如果有this就是绑定的对象。
- 是否有隐式绑定,如果有this就是那个上下文对象;
- 如果到了这一步,就是默认绑定,非严格模式下this指向全局对象。
三 箭头函数中的this
普通函数中的this已经基本弄清了,下面看看ES6中箭头函数的this。 首先要明白一点,箭头函数并不绑定this,箭头函数本身是没有this的。那它的this哪里来的呢?向外层寻找得到的,是的,和查询普通的词法作用域变量一样,它的this基于词法作用域,是从外层作用域得到的:
var a = 'global';
var fn = ()=>{console.log(this.a);}
var foo = function(){
var fn1 = fn;
var fn2 = ()=>{console.log(this.a)};
fn1();//'global'
fn2();//'obj'
}
foo.call({a:'obj'});
复制代码
这里fn1函数中的this从全局作用域中得到,fn2函数中的this从foo函数作用域中得到,也就是说foo的this是哪个,fn2函数的this就是哪个。
四、小结
本文主要总结了JavaScript中判断this指向的四条规则,在函数调用位置利用这四条规则可准确判断出this的指向,这四条规则按优先级排序为:
- new绑定——this指向新构建的对象;
- 显式绑定(call(...)、apply(...)、bind(...))——this指向绑定的对象;
- 隐式绑定——this指向上下文对象;
- 默认绑定——this指向全局对象window。
- 箭头函数不绑定this,它从外层作用域中寻找this
欢迎大家访问我的个人博客:简--我的博客主页