对箭头函数的 this 深度理解,它究竟指向谁?

简介

    面向对象编程语言中 this 是一个很是重要的关键字,其在函数执行中经过 this 来明确操做的对象,在 JS 中,JS 并不是面向对象语言,可是他也有 this 关键字,用来指向函数的调用对象,博主在学习 es5 的时候对 this 理解很是容易,由于之前学过面向对象语言,也曾大量的使用,可是学习到 es6 的箭头函数,它的 this 指向就让我有点困惑,因而花了一些时间从各个技术博客, MDN 文档,还有一些其余的资料进行了研究,总算是对箭头函数的 this 指向有个深入的认识了,下面就会讲述下我本身对箭头函数 this 的理解。前端

this 的重要性

this 在编程语言中用的次数很是多,多到你不知不觉就会下意识的写出 this,好比事件绑定,对事件源的操做,好比对一个对象的相关属性操做,继承中的 this 使用等。因此 this 的指向是一个一直须要理解并掌握的一个知识,若是不清楚 this 的指向,那么不少方法就会出现大问题,而且非语法的 bug 维护起来更是使人头大。严格模式中的 this 在全局中指向 undefined,其余地方下并无什么影响,因此一下论点也只讨论在非严格模式下的 this 指向es6

普通函数中的 this

普通函数中的 this 很好理解,无非如下四点:编程

  • 直接调用函数,this 指向全局 window
  • 对象调用函数,this 指向这个对象
  • 构造函数中的 this 指向将要实例化的对象
  • call,apply,bind 能够改变函数执行时内部的 this 指向

总结一句话就是谁调用函数,this 就指向谁,普通函数的 this 取决于执行时的函数。app

箭头函数中的 this

下面重点介绍下箭头函数中的 this 问题编程语言

箭头函数的语法:函数

// 无参数直接输出一句话
var fun1 = () => console.log('hello');
// 有一个参数并返回 x*x
var fun2 = x => x*x;
// 有一个参数并返回 y*y
var fun3 = (y) => y*y;
// 有两个参数并返回 x+y,也能够简写 (x,y) => x+y;
var fun4 = (x,y) => {
	return x+y;
}
复制代码

若是箭头函数中没有用到 this 的话,那么大可放心的直接使用,由于代码写的更少更方便,但若是须要 this,那么必定得清楚箭头函数中 this 指向谁。箭头函数的 this 指向也有不一样的说法,下面列举出不一样说法。学习

  • 箭头函数中没有单独的this,this值取决于箭头函数所在的环境
  • 箭头函数没有本身的this, 它的this是继承而来; 默认指向在定义它时所处的对象(宿主对象)
  • 箭头函数的 this 遵循词法做用域,指向其所属环境的执行上下文(也能够说是宿主对象)。

第一种说法较为模糊,概念不是那么的清晰,第二种说法经过继承而来有点牵强的感受,并且我我的觉的有点误导的感受,所以我看到第三种说法时,虽然以为有点陌生,说法很是官方的感受,可是却以为自习深刻了解第三种说法,应该能彻底掌握箭头函数的 this,因此就仔细研究了一下,下面将展开对第三种说法的论点,也是本篇的核心(前面一堆废话,凑字数:huaji:)ui

词法做用域,执行上下文

词法做用域简单来讲指的是函数做用域的一种工做模式,因此词法做用域的法则是基于做用域的概念。ES6 以前做用域分为全局做用域、局部做用域,变量遵循词法做用域,ES6 引入了块级做用域,使得JS也能像其余的编程语言有了真正的块级代码。执行上下文其实就是执行环境,也就是当前的 this,这里有点绕了吧,其实不要紧,下面会说明的,只不过此时是把上面的第二种继承方式的原理说明了,箭头函数的 this 就取决于这个执行上下文的 this,所以才说他是经过继承而来。this

有了上面的知识做为根基,那么究竟怎么理解此法做用域,和执行上下文,以及若是肯定箭头函数的 this 指向,接下来继续说明。 先来一段代码:es5

var num = 100;
var obj = {
    fun1function ({
        num = 200;
        console.log(num);
    },
    fun2function ({
        var num = 300;
        console.log(num)
    }
}
obj.fun1();         // No.2 200
obj.fun2();         // No.3 300
console.log(num);   // No.1 200
复制代码

从上述代码中,fun1 执行时为 num 赋值,可是能够从全局中寻找到 num,所以对全局的 num 进行操做,fun2 执行时在本身的局部做用域(函数)声明了一个 num,此时的 num 为局部的,与全局的 num 无关,fun2 执行完毕后局部 num 就消失了,因此全局的 num 最终结果为200,这一段代码中的变量使用的法则,遵循的就是此法做用域,说白点就是寻找变量的过程和其生命周期的范围受此法做用域约束,另外一个隐藏的知识点就是 obj.fun1(),obj.fun2() 执行时的执行上下文就是 obj, this 就是obj,执行环境就是 obj。


如今真正进入箭头函数的 this 讨论,若是有点忘了箭头函数 this 指向的第三种说明,如今能够立马向上翻滚看一下 :huaji:

看以下 Demo:

var obj = {};
var fun1 = function () {
	console.log(this);
}
var fun2 = () => {
	console.log(this);
}
console.log('normal------------');
// 全局环境下直接调用
fun1();
fun2();
console.log('call------------');
// 经过 call 进行执行环境的绑定
fun1.call(obj);
fun2.call(obj);
复制代码

image.png
从执行结果来看,call 不会对箭头函数进行绑定影响,也就是说箭头函数从他定义的那一刻时,它的 this 就已经肯定了,没法经过 call 更改,apply 也是一样的。

再看下一段代码:

var obj = {
	fun1: function () {
		console.log(this);
	},
	fun2: () => {
		console.log(this);
	}
}
obj.fun1();
obj.fun2();
复制代码

image.png
结果看出普通函数执行时this取决于执行环境(执行上下文)也就是 obj,而箭头函数的 this 却指向 window,使用 call 能改变吗?

obj.fun1();
obj.fun2();
obj.fun2.call(obj);
复制代码

image.png
很显然不能。 根据第三种说法解释: 箭头函数的 this 指向也遵循**词法做用域**,指向当前环境的**执行上下文**

  • 当前的词法做用域:依赖做用域,当前做用域是全局做用域。
  • 当前环境上下文:全局做用域的环境上下文 this 就是 window

再来一段代码巩固下:

var obj = {
	fun1: function () {
		setTimeout(function () {
			console.log('普通函数', this);
		})
	},
	fun2: function () {
		setTimeout(() => {
			console.log('箭头函数', this);
		})
	}
}
obj.fun1();
obj.fun2();
复制代码

image.png

  • 普通函数没得说,计时器时间到执行回调函数的话则在全局环境中执行,所以 this 指向 window
  • 箭头函数(this 在箭头函数定义时就肯定,遵循词法做用域,指向执行上下文对象(也能够说宿主对象)))
    • 词法做用域:当前所属环境为局部做用域,由于被定义在 function 内
    • 执行上下文:function 的执行上下文未来是在 obj 环境(除非用 call,apply,后面还会说明), 因此 this 已经在箭头函数定义时被绑定为 obj 了。又由于 fun2 的 this 指向的是 obj、箭头函数经过此法做用域依赖 fun2,因此才会有那个第二种说法说箭头函数的 this 会继承执行环境的执行上下文。

再来最后一段代码:

var obj = {
	// 普通函数中定义一个当即执行函数输出 this
	fun1: function () {
		(function () {
			console.log(this);
		})();
	},
	// 普通函数中定义一个当即执行箭头函数输出 this
	fun2: function () {
		(() => {
			console.log(this);
		})();
	},
	// 箭头函数中定义一个当即执行的普通函数输出 this
	fun3: () => {
		(function () {
			console.log(this);
		})();
	},
	// 箭头函数中定义一个当即执行的箭头函数输出 this
	fun4: () => {
		(() => {
			console.log(this);
		})();
	}
}
复制代码
  • 正常经过 obj 调用的结果:
console.log('正常执行------------');
obj.fun1(); // window
obj.fun2(); // obj
obj.fun3(); // window
obj.fun4(); // window
复制代码

image.png

  • obj.fun1():内部当即执行函数由于直接调用,执行环境为 window 因此 this 是 window
  • obj.fun2():内部的当即执行箭头函数由于定义时 this 根据词法做用域绑定执行上下文,所以箭头函数的做用域为 fun2,绑定 fun2 的执行上下文,this 绑定为 obj
  • obj.fun3():能够不分析箭头函数,由于内部当即执行的普通函数直接调用,执行环境是 window,this 指向 window
  • obj.fun4():分析步骤同 fun2 分析,内部的当即执行箭头函数根据词法做用域约束,其属于 obj.fun4 的箭头函数,要绑定 obj.fun4 所在的执行上下文,但由于 obj.fun4 也是一个箭头函数,因此也一样受词法做用域的约束,根据以前的示例,obj.fun4 的执行上下文指向的 window,所以内部的当即执行箭头函数也指向的是 window
  • 经过 call 强行改变执行环境的结果:
// 下面 this 是全局的 window
console.log('使用call执行------------');
obj.fun1.call(this); // window
obj.fun2.call(this); // window
obj.fun3.call(this); // window
obj.fun4.call(this); // window
复制代码

image.png
从结果上来看,只有 fun2 的结果被改变了,其余的结果没有影响,根据上面总结的判断方法,应该能够自行对除 fun2 的其余结果进行分析,那么如今回顾下 fun2 的代码
image.png
为何箭头函数里的 this 发生了变化!前面提到过箭头函数必定定义后就会绑定 this,是没法经过 call 和 apply 进行改变,为何这里发生了变化?是否是这里比较特殊,不会遵循箭头函数 this 的指向规则? 其实并非,这里仍然遵循以前说的法则,正由于它遵照规则,因此输出的 this 发生了变化,只不过这里绕了一个弯,由于这里是函数内部,这里的当即执行普通剪头函数在每次 fun2 调用时会从新进行一次函数的定义,而后执行,这里 fun2 的代码等价于

fun2: function () {
	var testFun = () => {
		console.log(this);
	}
	testFun();
}
复制代码

再次根据箭头函数 this 绑定的法则来看(箭头函数的 this 遵循词法做用域,指向其所属环境的执行上下文(也能够说是宿主对象)。),每当 fun2 被调用时,会从新定义箭头函数,当前箭头函数的词法做用域是 fun2,其指向 fun2 的执行上下文,正常状况是 obj,但咱们经过 obj.fun2.call(this) 强行改变了 fun2 的执行上下文为 window,因此 fun2 的箭头函数从新定义时则指向了 fun2 的执行上下文 window,也就是经过 call 的结果,因此这并不矛盾。 验证代码以下:

var obj = {
	// 普通函数中定义一个当即执行箭头函数输出 this
	fun2: function () {
		(() => {
			console.log(this);  // 正常调用 fun2 时,this 已经给被绑定为 obj 了
		}).call(window);        // 没法经过 call 强行绑定 window
	},
}
obj.fun2();
复制代码

image.png
结果并无强行改变箭头函数的 this ,证实上面说法时正确的。 同理,假如将 fun2 里面的当即执行函数改为计时器 + 箭头函数的格式,那么每次也是调用 fun2 从新生成计时器和箭头函数,箭头函数的内部 this 照样依据词法做用域绑定执行上下文。


若是读者看到哪里有误或哪里说的模糊还请说明,我会及时学习更正的,我也只是一个刚入门的前端小白,这也只是个人我的理解,也是第一次在掘金发布的本身文章,但愿大佬们不要嫌弃我

相关文章
相关标签/搜索