关于JavaScript中this指向的问题

js中的this是需要在一定的上下文环境下的,所以this都是在函数内出现,但是this会因场景的不同产生不同的指向。

箭头函数

箭头函数是es6提出的特性,它与普通的函数的一大区别就是this的指向问题。

箭头函数并没有它自己的this指向,它的this指向取决于定义它时所处的外部函数的this指向。也就是说箭头函数的this指向在它被定义时就确定了,这一点在babel编译就可以体现出来。

编译前:

1
2
3
4
5
function fn() {
const fn1 = () => {
console.log(this)
}
}

编译后:

1
2
3
4
5
6
7
function fn() {
var _this = this;

var fn1 = function fn1() {
console.log(_this);
};
}

所以说要确定箭头函数的this指向,就应该先弄清楚它所处的外层函数的this指向。

new关键字

在使用new关键字调用函数时,函数的this指向一定是这个新创建的对象(箭头函数不可以new)。

这个就比较常见了,当我们自己定义一个构造函数并用这个构造函数创建对象时经常会使用这个特性,例如:

1
2
3
4
5
function Animal(name) {
this.name = name
}
const cat = new Animal('Cat')
console.log(cat) // { name : 'Cat' }

而让我对this这个问题产生兴趣开始研究的也正是new关键字的this特性。

学习过vue的朋友都知道,vue2中的响应式数据是通过Object.defineProperty实现的,在学习这里的知识时,在网上看到了完成简单Observer监视器的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let data = {
name:'张三',
age:18
}
// 创建一个监视的实例对象,用于监视data中的属性变化
const obs = new Observer(data)
console.log(obs);
let vm = {};
vm._data = data = obs;
function Observer(obj){
// 汇总对象中所有的属性形成一个数组
const keys = Object.keys(obj);
// 遍历
keys.forEach((k)=>{
Object.defineProperty(this, k, {
get(){
return obj[k]
},
set(val){
console.log(`${k}被修改了,要去解析模板,生成虚拟dom...`);
obj[k] = val
}
})
})
}
ript>

这个this指向当时一直没搞懂,后来仔细研究后才想起来这就是最基本的new关键字的this指向。

apply和call

applycall的第一个参数都是你所要修改的this,区别在于apply调用传参时,参数需要放在数组内,而call就可以和原函数的传参方式保持一致使用逗号分隔。

1
2
3
4
5
6
7
8
const p1 = {}
const p2 = {}
function Person (name, age) {
this.name = name;
this.age = age;
}
Person.apply(p1, ['John', 20]) // { name : 'John', age : 20}
Person.call(p2, 'Tome', 18) // { name : 'Tom', age : 18}

但是call和apply是无法改变箭头函数的this指向

bind

bindapplycall的功能差不多,都是可以改变this的指向,但是bind的返回值是一个函数。而在这个函数体内,调用了你所指定的函数,并且改变了this的指向。

可以理解为返回了一个函数,而这个函数的内部调用了apply或者call改变this指向。

1
2
3
4
// 大概可以理解为
BindFunction (参数) {
fn.call(所指定的this,参数)
}

bind的举例

1
2
3
4
5
6
7
8
const p = {}
function Person(name, age) {
this.name = name;
this.age = age;
}
const PersonBind = Person.bind(p)
PersonBind('John', 30)
console.log(p)// {name: 'John', age: 30}

bind有两个特性:

  1. 多次调用bind时,只认第一个bind的值
  2. 同样地,箭头函数的this不会被bind改变

xxx.xxx()

这种是我们最常用的调用方式了,规则当然就是谁调用的this就指向谁。但是它的优先级低于以上所描述的this指向

1
2
3
4
5
6
7
8
const p = {
a: 1,
fn(){
this.a ++
}
}
p.fn()
console.log(p.a); // 2

直接调用

直接调用的this指向的都是对应环境的全局对象,浏览器中是Window,Node.js中是Global

1
2
3
4
5
function fn() {
console.log(this)
}

fn() // Window

回调函数中的this

回调函数都是基本都是直接调用的,所以它们的this指向都是对应的全局对象。但是如果回调函数是箭头函数那么情况会有所不同。

下面我们举例

箭头函数中:

1
2
3
4
5
6
7
8
9
10
const obj = {
name: '张三',
age: 18,
timeOut () {
setTimeout(() => {
console.log(this.name)
}, 1000)
}
}
obj.timeOut() // 一秒后输出 张三

普通函数中:

1
2
3
4
5
6
7
8
9
10
const obj = {
name: '张三',
age: 18,
timeOut () {
setTimeout(function(){
console.log(this.age)
}, 1000)
}
}
obj.timeOut() // 一秒后输出 undefined

第二个情况就不用多说了,之所以输出undefined就是因为在回调函数直接调用,this为Window。

但是第一个情况中this指向了obj,这是因为回调函数可以理解为是处于该函数的外部的一个函数,即为该回调函数属于timeOut函数的作用域。而this的指向不受外界的影响,与timeOut保持一致所以它的指向为obj。

不在函数内

不在函数中的场景,可分为浏览器的 <script /> 标签里,或 Node.js 的模块文件里。

  1. <script /> 标签里,this 指向 Window。
  2. 在 Node.js 的模块文件里,this 指向 Module 的默认导出对象,也就是 module.exports