JavaScript中this的判定准则——不再惧怕this

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操作符调用时,我们称之为「构造函数调用」。发生构造函数调用时执行了以下步骤:

  1. 构建一个全新的对象;
  1. 对对象进行[[prototype]]连接;
  2. 将这个新的对象绑定到函数的this上;
  3. 如果构造函数调用没有返回其他对象,将自动返回这个创建的对象。
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时就有据可循了——找到函数的调用位置,然后按照以下顺序判断:

  1. 是否有new调用,如果有this就是新创建的对象。
  2. 是否有显式绑定,如果有this就是绑定的对象。
  3. 是否有隐式绑定,如果有this就是那个上下文对象;
  4. 如果到了这一步,就是默认绑定,非严格模式下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的指向,这四条规则按优先级排序为:

  1. new绑定——this指向新构建的对象;
  1. 显式绑定(call(...)、apply(...)、bind(...))——this指向绑定的对象;
  2. 隐式绑定——this指向上下文对象;
  3. 默认绑定——this指向全局对象window。
  4. 箭头函数不绑定this,它从外层作用域中寻找this

欢迎大家访问我的个人博客:简--我的博客主页

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值