区别多种this情况

70 阅读4分钟

涵义

简单说,this就是属性或方法“当前”所在的对象。

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。

我们定义一个对象,对象的属性值可能是一个函数。

var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x;在obj环境执行,this.x指向obj.x。

this不是执行上下文(EC才是执行上下文),this是执行主体。this的执行主体是对象,无论是全局对象window,还是自己定义的对象。而执行上下文一般由作用域构成,执行时沿作用域链查找。

如下面的函数f执行,由于是在全局环境下执行下,this是window,而函数的作用域是EC(f),作用域链是EC(f)-->EC(G)。

var f = function () {};
// 单独执行
f()

如何区分执行主体

事件绑定

事件绑定:给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的THIS是当前元素本身(特殊:IE6~8中基于attachEvent方法实现的DOM2事件绑定,事件触发,方法中的THIS是WINDOW而不是元素本身)。

基于DOM0和DOM2进行事件绑定:

let body = document.body;
body.onclick = function () {
	// 事件触发,方法执行,方法中的THIS是BODY
	console.log(this);
};
body.addEventListener('click', function () {
	console.log(this); //=>BODY
});

// IE6~8中的DOM2事件绑定
box.attachEvent('onclick', function () {
    console.log(this); //=>WINDOW
}); 

普通方法执行

普通方法执行(包含自执行函数执行、普通函数执行、对象成员访问调取方法执行等):只需要看函数执行的时候,方法名前面是否有“点”,有“点”,“点”前面是谁THIS就是谁,没有“点”THIS就是WINDOW(非严格模式)/UNDEFINED(严格模式)。

(function () {
	console.log(this); // window
})();
let obj = {
	fn: (function () {
		console.log(this); // window
		return function () {}
	})() //把自执行函数执行的返回值赋值给OBJ.FN
};

function func() {
	console.log(this);
}
let obj = {
	func: func
};
func(); //  方法中的THIS:WINDOW
obj.func(); //  方法中的THIS:OBJ
[].slice(); 

上面的代码中数组实例基于原型链机制,找到ARRAY原型上的SLICE方法([].slice),然后再把SLICE方法执行,此时SLICE方法中的THIS是当前的空数组。

Array.prototype.slice();

此时SLICE方法执行中的THIS:Array.prototype。

function func() {
	// THIS => WINDOW
	console.log(this);
}
document.body.onclick = function () {
	// THIS => BODY
	func();
};

上面的代码虽然给document.body点击事件绑定了一个回调函数,但是里面的函数执行this前面没有点,也就是说被拿到了全局环境下执行,所以this是window

构造函数

构造函数执行(NEW XXX):构造函数体中的THIS是当前类的实例。

构造函数体中的THIS在“构造函数执行”的模式下,是当前类的一个实例,并且THIS.XXX=XXX是给当前实例设置的私有属性

function Func() {
	this.name = "F";
	console.log(this); 
}

而原型上的方法中的THIS不一定都是实例,主要看执行的时候,“点”前面的内容。

Func.prototype.getNum = function getNum() {
	console.log(this);
};

let f = new Func;
f.getNum(); // this=>实例f
f.__proto__.getNum(); //this=>f.__proto__
Func.prototype.getNum();  //this=>Func.prototype

箭头函数

ES6中提供了ARROW FUNCTION(箭头函数): 箭头函数没有自己的THIS,它的THIS是继承所在上下文中的THIS。

let obj = {
	func: function () {
		console.log(this);
	},
	sum: () => {
		console.log(this);
	}
};
obj.func(); //=>THIS:OBJ
obj.sum(); //=>THIS是所在上下文(EC(G))中的THIS:WINDOW
obj.sum.call(obj); //=>箭头函数是没有THIS,所以哪怕强制改也没用  THIS:WINDOW

小诀窍:箭头函数的this一般是继承所在上下文中的this,而上下文一般是由函数形成的私有作用域和包含window的全局作用域。所以找箭头函数的this先看她是不是被包含在一个普通函数里,如果是,那就找包含它的函数的this。如果不是,基本上就是在全局环境下,this=》window。

于CALL/APPLY/BIND等方式

可以基于CALL/APPLY/BIND等方式,强制手动改变函数中的THIS指向:这三种模式是和直接很暴力的(前三种情况在使用这三个方法的情况后,都以手动改变的为主)。

let obj = {
	i: 0,
	func() {
		setTimeout(function () {
			// 基于BIND把函数中的THIS预先处理为OBJ
			this.i++;
			console.log(this);
		}.bind(this), 1000);
	}
};
obj.func();

总结

this是js的难点之一,学号this对于我们写出优质的js代码有非常大的帮助,所以我写下这篇博客总结我们工作和学习中常见的遇到的this的情况,当然,这篇博客的举例十分有限,不可能面面俱到,但是可以举一反三,对于处理this问题非常有帮助。