总结 JavaScript 中的 this 指向问题
Overview
初学 JavaScript 的时候,时常对于代码中 this
的值感到困惑,因为 JavaScript 中的 this
的值总是不确定的,每次都要将 this
的值打印出来才能放心。
实际上 this
具体指向哪个对象是在函数运行时的环境动态绑定的,而非函数被声明时的环境。
尽管如此,总还是有规律可循的,我们可以将 this
的场景大致分为下面 5 种:
- 作为普通函数调用
- 作为对象的方法调用
- 作为构造器调用
- 通过
call
和apply
调用 - 箭头函数
在 ES6 标准新增的箭头函数中的
this
是由被声明的环境即上下文来确定的。
情况一:作为普通函数调用
这可能是我们学习过程中最先接触到的一种函数类型。
1) 普通函数:
当函数作为普通函数或者匿名函数调用时,this
总是指向全局对象,在浏览器中这个全局对象是 window
,而在 nodejs 中全局对象是 global
,这里讨论的是浏览器端,所有代码都可以在 chrome 开发者工具的 console 面板中执行。
1window.name = 'globalName'
2
3function printName1 () {
4 console.log(this === window)
5 console.log(this.name)
6}
7printName1()
8// true
9// "globalName"
ES5 规范提出了「严格模式」,启用方式是在整个脚本文件第一行或者函数内第一行添加一条语句 'use strict'
即可开启严格模式。
如果启用「严格模式」,那么 this
就不再是指向全局对象,而是 undefined
。
1function printName2 () {
2 "use strict" // 启用严格模式
3 console.log(this) // undefined
4}
5
6printName2()
2) 匿名函数:
匿名函数自执行,这种形式封装公用库的时候最常见:
1window.name = 'globalName'
2(function () {
3 console.log(this === window) // true
4 console.log(this.name) // "globalName"
5})()
和普通函数一样,匿名函数中的 this
也是指向全局对象。
作为参数的匿名函数:
1window.name = 'globalName'
2
3setTimeout(function () {
4 console.log(this === window) // true
5 console.log(this.name) // "globalName"
6}, 100)
7
8[1, 2, 3].forEach(function (item) {
9 console.log(item, this.name)
10 // 1 "globalName"
11 // 2 "globalName"
12 // 3 "globalName"
13})
情况二:作为对象的方法调用
当函数作为对象当属性方法调用时,this
总指向这个对象。
1var obj = {
2 name: 'obj',
3 printName: function () {
4 console.log(this === obj) // true
5 console.log(this.name) // "obj"
6 }
7}
8
9obj.printName()
但是如果一个对象的属性方法又赋值给了其他变量,那么 this
将发生变化,其指向只有在函数执行那一刻才能确定。例如:
1var obj = {
2 name: 'obj',
3 printName: function () {
4 console.log(this === obj) // true
5 console.log(this.name) // "obj"
6 }
7}
8
9var myPrintName = obj.printName
10window.name = 'globalName'
11
12myPrintName()
13// false
14// "globalName"
当 myPrintName
执行时,就要按照普通函数来判断 this
指向了。
再来一个例子:
1var obj = {
2 name: 'obj',
3 printName: function () {
4 console.log(this === obj) // true
5 console.log(this.name) // "obj"
6 }
7}
8
9var obj2 = {
10 name: 'obj2',
11}
12obj2.printName = obj.printName
13
14obj2.printName()
15// false
16// "obj2"
当 obj2.printName
执行时,printName
是作为 obj2
的属性方法来调用的,因此 this
指向 obj2
这个对象。
所以说,JavaScript 中的 this 指向无法在定义时判断,只有在其执行时才能判断。
1var printName () {
2 console.log(this === window)
3 console.log(this.name)
4}
情况三:作为构造器调用
在 JavaScript 中没有「类」的概念(直到 ES6 才有类的出现),而是把函数作为构造器,通过 new 操作符来生成实例。
那么在构造函数中 this
就指向新生成的实例。
1var MyClass = function () {
2 // 给实例添加 name 属性
3 this.name = 'myclass'
4 this.printName = function () {
5 return this.name
6 }
7}
8var obj = new MyClass()
9obj.printName() // "myclass"
情况四:显式指定 this
前面的几种方法都是被动地根据代码执行时的环境来判断 this
具体指向哪里,那么有没有办法主动指定 this
指向呢。
答案当然是有的,甚至它们的出场率还相当高。
常见的显式指定 this
的方法主要是 call
,apply
和 bind
来,在函数式编程中几乎离不开这三个方法。
先定义如下变量:
1window.name = 'globalName'
2function printName () {
3 console.log(this.name)
4}
5
6var obj1 = { name: 'obj1' }
7var obj2 = { name: 'obj2' }
8var obj3 = { name: 'obj3' }
默认情况 this 指向全局对象 window
1printName() // "globalName"
使用 call
来改变 this
指向:
1printName.call(obj1) // "obj1"
使用 apply
来改变 this
指向:
1printName.apply(obj2) // "obj2"
使用 bind
来改变 this
指向:
1var printName2 = printName.bind(obj3)
2printName2() // "obj3"
除此之外,还有一些函数也可以修改 this 指向,例如:forEach, map, filter, some, every 等。以 forEach
为例:
1var obj = { name: 'zwc' }
2var arr = [ 1, 2 ]
3
4arr.forEach(function (item, index) {
5 console.log(item, this)
6})
7// 1 Window
8// 2 Window
9
10arr.forEach(function (item, index) {
11 console.log(item, this)
12}, obj)
13// 1 {name: "zwc"}
14// 2 {name: "zwc"}
forEach
第一个参数接收一个函数作为迭代器,用来处理数组中每一项元素,这个函数通常是一个匿名函数,函数内部的 this
指向全局对象。
forEach
第二个参数可以接收一个对象,这个对象就是参数函数中的 this
指向。
根据打印结果可以看到,在 forEach
的迭代器函数中的 this
已经指向了 obj
。
情况五:ES6箭头函数
箭头函数简介
ES6 允许使用「箭头」(=>
)定义函数。
1var f = v => v
2
3// 等同于
4var f = function (v) {
5 return v
6}
除了形式更简洁之外,箭头函数没有自己的 this
,而是从自己作用域链的上一层继承 this
。
箭头函数总是从自己作用域链的上一层继承
this
。
神马意思呢?我的理解就是箭头函数内部的 this
指向永远是箭头函数被定义时所在的作用域的 this
,并且无法修改。
无法绑定 this
使用 call 来调用箭头函数时,第一个参数会被忽略,也就是说无法修改 this 指向。apply 和 bind 也是同样现象。
1var printName = () => {
2 console.log(this === window) // true
3}
4
5var obj = { name: 'obj' }
6
7printName.call(obj)
例子一
来看一个例子,在不使用箭头函数的情况下,我们知道构造器函数内部 this 指向对象实例,而匿名函数的 this 是指向全局对象的,因此想要通过定时器打印对象实例的 age
属性,只能用一个变量 self
保存 this
的引用(即闭包)
1function Person(){
2 // 构造器函数内部 this 指向对象实例
3 this.age = 0
4 var self = this
5 setInterval(function () {
6 // 匿名函数中 this 指向全局对象
7 console.log(self.age++)
8 }, 1000)
9}
10
11var p = new Person()
使用箭头函数之后,因为箭头函数的 this
继承自其被定义时所在环境的 this
,在本例中这个 this 就是实例对象:
1function Person(){
2 // 构造器函数内部 this 指向对象实例
3 this.age = 0
4 setInterval(() => {
5 // 这里的 this 也指向构造函数的 this
6 console.log(this.age++)
7 }, 1000)
8}
9
10var p = new Person()
例子二
再说一个更实用的例子:在 Vue.js 中使用箭头函数
1import axios from 'axios'
2export default {
3 methods: {
4 fetch () {
5 axios.get('/userinfo')
6 .then(resp => {
7 this.sayHi() // this 指向 vue 实例
8 })
9 .catch(err => {
10 this.sayHi() // this 指向 vue 实例
11 })
12 },
13 sayHi () {
14 setTimeout(() => {
15 // this 指向 vue 实例
16 }, 1000)
17 }
18 }
19}
使用箭头函数之后,再也无需缓存 vue 实例,像是 var vm = this
这种代码统统可以消灭掉,嗯,清爽!
相关链接
总结
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的 this
值:
- 如果是该函数是一个普通函数或者匿名函数
- 在严格模式下的函数调用下,
this
指向undefined
, - 在非严格模式的函数调用中,
this
指向全局对象,浏览器中全局对象是window
,在 nodejs 中全局对象是global
- 在严格模式下的函数调用下,
- 如果是该函数是一个构造函数,
this
指针指向一个新的对象(实例) - 如果是该函数是一个对象的方法,则它的
this
指针指向这个对象 - 或者使用
call
apply
等方法显式指定this
的指向
在箭头函数中,则是根据箭头函数上下文决定其 this 指向,且无法修改 this
指向。
本文完,感谢阅读。:stuck_out_tongue_winking_eye: