JavaScript中的this指向问题

一、前言

深入学习的过程是 快乐 的!!!

面试问到了箭头函数和普通函数的区别,去阮一峰ES6中看相关资料,发现主要还是指出 this 的指向问题,于是去深入理解this,相关文章又提出了执行上下文,调用栈等概念,顺着这个线索学下去发现盲区越来越多。。

本文主要介绍JavaScript中 this 关键字,原文地址:js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解

二、绑定方式

在ECMAScript 6之前没有箭头函数,JavaScript中绑定方式主要包括四种,分别是 默认绑定隐式绑定 , 显式绑定new绑定 几种,ES6中提出了箭头函数,因此this绑定多出了一种形式,让我们一探究竟吧

1. 默认绑定

所谓的默认绑定,指的函数在全局作用域中运行时,this默认指向window对象的情况(非严格模式),严格模式下指向undefined

如下面例子(非严格模式):

1
2
3
4
5
6
var name = "rambler";
function test() {
console.log(this); // window对象
console.log(this.name) // rambler
}
test();

严格模式下的结果:

1
2
3
4
5
6
7
var name = "rambler";
function test() {
"use strict"
console.log(this); // undefined
console.log(this.name) // Uncaught TypeError: Cannot read property 'name' of undefined
}
test();

严格模式 只是在ECMAScript 5中提出的用在脚本或者函数顶部的一种指令或者声明,有点类似html文档头部的 <!DOCTYPE html> 声明

在严格模式下代码有一些限制,比如未声明不能赋值,不能通过 delete删除变量和属性等,这里不一 一罗列

2. 隐式绑定

什么是隐式绑定呢,如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,如果函数调用前存在多个对象,this指向距离调用自己最近的对象

1
2
3
4
5
6
7
8
function fun() {
console.log(this.name);
}
let obj = {
name: "test",
fun: fun
}
obj.fun(); // test

可能这样写更容易理解?

1
2
3
4
5
6
7
let obj = {
name: "test",
fun: function() {
console.log(this.name);
}
}
obj.fun(); // test

其实上面说的多个对象调用this指向最近的对象,和这个是一个意思,例子:

1
2
3
4
5
6
7
8
9
10
let obj = {
name: "外层",
test: {
name: "内层",
fun: function() {
console.log(this.name);
}
}
}
obj.test.fun(); //内层

文章原文中指出了一个问题,将是将函数引用赋值给一个变量,通过这个变量调用这个函数this将丢失

看这个例子

1
2
3
4
5
6
7
8
9
let test = {
name: "内层",
fun: function() {
console.log(this.name);
}
}
let fun = test.fun;
fun(); // 没有输出
test.fun(); // 内层

将test.fun赋值给fun,在通过fun()调用this就失效

其实这里可以这样理解,test.fun是一个函数的引用,fun这个变量就是保存的这个函数的引用,直接通过fun调用,就相当于全局执行这个函数,类似这样

1
2
3
4
function fun() {
console.log(this.name);
}
fun();

修改代码,打印this,会发现通过变量引用对象里面的函数,this是指向window对象,印证了我的说法

但是存在一个问题就是不太明白为什么打印的空,而不是undefined,因为window对象中明明没有name属性

3. 显式绑定

所谓的显式绑定就是指主动绑定函数调用时的this,主要时通过apply,call和bind进行绑定,三者的区别不是本文的重点,只将this的问题

上个简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fun() {
console.log(this.name);
}
let obj1 = {
name: "apply方式绑定"
}
let obj2 = {
name: "call方式绑定"
}
let obj3 = {
name: "bind方式绑定"
}
fun.apply(obj1); // apply方式绑定
fun.call(obj2); // call方式绑定
let newFun = fun.bind(obj3);
newFun(); // bind方式绑定

通过这三中方式手动绑定this,这里需要注意的是call和apply绑定this后会自动执行,而bind绑定后返回的是一个函数,可以通过一个变量接受,并执行,或者直接在后面加上括号运行,fun.bind(obj3)()

bind是硬绑定,也就是说通过bind绑定this后,后续无论是通过bind还是apply都无法修改this指向

4. new 绑定

准确来说,js中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。new的过程如下:

比如我声明一个Person构造函数

1
2
3
4
function Person() {
this.id = "123";
}
Person.prototype.name = "test";

然后通过new关键字创建一个Person的 实例

1
var person = new Person

这期间都发生了什么呢?实际过程如下

1
2
3
4
// 伪代码
var person = {}; // 创建一个空对象
person.__proto__ = Person.prototype; // 将person对象的原型链指向Person的原型链
Person.call(person); // 通过call改变this指向person对象

这就很容易理解new中this绑定在谁身上了,也就是刚创建的对象

通过打印创建的person对象发现person对象有id属性,证明this确实指向person对象

5. 箭头函数绑定

箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁。而且一旦绑定,便无法修改,类似bind硬绑定

记住一句话, 箭头函数就是个吃软饭的,外层this指向谁,他的this就指向谁

简单例子:

1
2
3
4
5
6
7
8
var name = "111"
let fun = () => {
console.log(this.name);
}
let obj = {
name: "test"
}
fun.call(obj);

这个箭头函数fun是在全局作用域中创建的,因此this指向window对象,所以打印的结果不是test,而是window对象的name属性,也就是111

第二个例子

1
2
3
4
5
6
7
8
9
10
function fun() {
var test = () => {
console.log(this.name)
}
return test;
}
var obj = {
name: "test",
}
fun.call(obj)();

注意:箭头函数所在函数this指向谁,箭头函数的this就指向谁

上面例子最后一行,fun.call(obj)这句话通过显式绑定将fun函数的this指向obj对象,fun函数中又返回了一个箭头函数,所以箭头函数也指向obj对象,因此打印结果是 test

三、this绑定优先级问题

new无法和显式绑定同时存在,因为通过new操作符调用函数返回的是一个对象,而不是一个函数,因此无法在通过call和apply等更改this指向

例如下面的代码会报错

1
2
3
function Person() {};
let obj = {};
new Person().call(obj);//Uncaught TypeError: (intermediate value).call is not a function

因此有下面的结论:

显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定