简介
面向对象编程语言中 this 是一个非常重要的关键字,其在函数执行中通过 this 来明确操作的对象,在 JS 中,JS 并非面向对象语言,但是他也有 this 关键字,用来指向函数的调用对象,博主在学习 es5 的时候对 this 理解非常容易,因为以前学过面向对象语言,也曾大量的使用,但是学习到 es6 的箭头函数,它的 this 指向就让我有点困惑,于是花了一些时间从各个技术博客, MDN 文档,还有一些其他的资料进行了研究,总算是对箭头函数的 this 指向有个深刻的认识了,下面就会讲述下我自己对箭头函数 this 的理解。
this 的重要性
this 在编程语言中用的次数非常多,多到你不知不觉就会下意识的写出 this,比如事件绑定,对事件源的操作,比如对一个对象的相关属性操作,继承中的 this 使用等。所以 this 的指向是一个一直需要理解并掌握的一个知识,如果不清楚 this 的指向,那么很多方法就会出现大问题,并且非语法的 bug 维护起来更是令人头大。严格模式中的 this 在全局中指向 undefined,其他地方下并没有什么影响,所以一下论点也只讨论在非严格模式下的 this 指向。
普通函数中的 this
普通函数中的 this 很好理解,无非以下四点:
- 直接调用函数,this 指向全局 window
- 对象调用函数,this 指向这个对象
- 构造函数中的 this 指向将要实例化的对象
- call,apply,bind 可以改变函数执行时内部的 this 指向
总结一句话就是谁调用函数,this 就指向谁,普通函数的 this 取决于执行时的函数。
箭头函数中的 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,所以就仔细研究了一下,下面将展开对第三种说法的论点,也是本篇的核心(前面一堆废话,凑字数)
词法作用域,执行上下文
词法作用域简单来说指的是函数作用域的一种工作模式,所以词法作用域的法则是基于作用域的概念。ES6 之前作用域分为全局作用域、局部作用域,变量遵循词法作用域,ES6 引入了块级作用域,使得 JS 也能像其他的编程语言有了真正的块级代码。执行上下文其实就是执行环境,也就是当前的 this,这里有点绕了吧,其实没关系,下面会说明的,只不过此时是把上面的第二种继承方式的原理说明了,箭头函数的 this 就取决于这个执行上下文的 this,因此才说他是通过继承而来。
有了上面的知识作为根基,那么究竟怎么理解此法作用域,和执行上下文,以及如果确定箭头函数的 this 指向,接下来继续说明。
先来一段代码:
var num = 100;
var obj = {
fun1: function () {
num = 200;
console.log(num);
},
fun2: function () {
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 指向的第三种说明,现在可以立马向上翻滚看一下
看如下 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);
从执行结果来看,call 不会对箭头函数进行绑定影响,也就是说箭头函数从他定义的那一刻时,它的 this 就已经确定了,无法通过 call 更改,apply 也是同样的。
再看下一段代码:
var obj = {
fun1: function () {
console.log(this);
},
fun2: () => {
console.log(this);
}
}
obj.fun1();
obj.fun2();
结果看出普通函数执行时 this 取决于执行环境(执行上下文)也就是 obj,而箭头函数的 this 却指向 window,使用 call 能改变吗?
obj.fun1();
obj.fun2();
obj.fun2.call(obj);
很显然不能。
根据第三种说法解释:箭头函数的 this 指向也遵循**词法作用域**,指向当前环境的**执行上下文**
- 当前的词法作用域:依赖作用域,当前作用域是全局作用域。
- 当前环境上下文:全局作用域的环境上下文 this 就是 window
再来一段代码巩固下:
var obj = {
fun1: function () {
setTimeout(function () {
console.log('普通函数', this);
})
},
fun2: function () {
setTimeout(() => {
console.log('箭头函数', this);
})
}
}
obj.fun1();
obj.fun2();
- 普通函数没得说,计时器时间到执行回调函数的话则在全局环境中执行,因此 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
- 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
从结果上来看,只有 fun2 的结果被改变了,其他的结果没有影响,根据上面总结的判断方法,应该可以自行对除 fun2 的其他结果进行分析,那么现在回顾下 fun2 的代码
为什么箭头函数里的 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();
结果并没有强行改变箭头函数的 this ,证明上面说法时正确的。
同理,假如将 fun2 里面的立即执行函数改成计时器 + 箭头函数的格式,那么每次也是调用 fun2 重新生成计时器和箭头函数,箭头函数的内部 this 照样依据词法作用域绑定执行上下文。
如果读者看到哪里有误或哪里说的模糊还请说明,我会及时学习更正的,我也只是一个刚入门的前端小白,这也只是我的个人理解
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于