0%

JavaScript中this的指向问题

面试常考题,很多初学者会被JavaScript中众多不同情况的this指向搞迷糊。本质上,this始终指向一个对象。

0 太长不看版

只要弄明白,this在函数中被使用,在定义时不确定指向,调用时才能确定指向(箭头函数除外),并始终指向一个对象,可以简单分为以下6种情况:

  1. 普通函数调用,指向全局对象
  2. 对象方法调用,谁调用指向谁,只看调用不看引用
  3. 构造函数调用,指向新构造的实例,尽管可能不被构造函数返回
  4. applycallbind,改变指向
  5. 匿名函数调用,一般指向全局对象
  6. 箭头函数调用,指向定义箭头函数作用域的this

1 普通函数调用

普通函数调用时,this指向全局对象,在浏览器中,全局对象为window。看示例:

1.1 var定义变量

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

fn作为普通函数被调用,this指向全局对象window

var定义的全局变量视为全局对象的属性,因此this.name输出全局对象windowname属性,即"var"

1.2 let定义变量

1
2
3
4
5
6
7
let name = "let";
function fn() {
let name = "fn-let";
console.log(this); // window对象
console.log(this.name); // undefined
}
fn();

let定义的变量不会成为全局对象的属性,因此this.name即全局对象windowname属性不存在,输出undefined

1.3 全局对象属性

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

毫无疑问,this.name输出全局对象windowname属性,即"win"

2 对象方法调用

函数作为对象方法被调用时,哪个对象调用,this就指向谁。

如果函数变量被赋值给其他变量,依然是谁调用指向谁,而不考虑函数来源(只看调用,不看引用)

2.1 对象直接调用自有方法

1
2
3
4
5
6
7
8
var obj = {
age: 15,
fn() {
console.log(this.name); // undefined
console.log(this.age); // 15
}
}
obj.fn();

obj对象直接调用自有方法fnfn中的this就指向调用它的obj对象。

2.2 对象多层调用自有方法

1
2
3
4
5
6
7
8
9
10
var obj = {
age: 15,
inner: {
age: 20,
fn() {
console.log(this.age);
}
}
}
obj.inner.fn(); // 20

尽管方法fn是被最外层对象obj间接调用,但fn中的this指向最近的上一层对象inner(即直接调用它的对象)。

2.3 函数变量被赋值给其他对象的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
age: 15,
fn() {
console.log(this.name);
console.log(this.age);
}
}
var obj2 = {
name: "obj2",
age: 20
}
obj2.fn2 = obj.fn;
obj2.fn2(); // obj2 20

尽管方法fn2来源于对象obj的方法fn,但依然是“谁调用指向谁”,方法被obj2调用,this就指向obj2

2.4 原型方法

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
age: 15
}
var obj2 = {
name: "obj2",
age: 20
}
Object.prototype.getAge = function () {
console.log(this.age);
}
obj.getAge(); // 15
obj2.getAge(); // 20

这个就很好理解了~虽然getAge是原型对象Object.prototype的方法,但实例objobj2进行了调用,方法中的this就指向它们。

实际上,这就是原型方法的特点之一:全体实例的可复用方法

2.5 函数变量被赋值给普通变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var name = "var";
var age = 123;
var obj = {
age: 15,
getName() {
console.log(this.name);
}
}
var obj2 = {
name: "obj2",
age: 20
}
Object.prototype.getAge = function () {
console.log(this.age);
}
var fn1 = obj.getName;
let fn2 = obj2.getAge;
fn1(); // var
fn2(); // 123

这时,fn1fn2实际上是作为普通函数调用,根据“谁调用指向谁”的原则,它们当中的this指向全局对象window,输出全局对象的属性值"var"123

总结:无论嵌套几层,无论如何赋值,无论函数来源,谁调用了函数/方法,this就指向谁。

3 构造函数调用

一般情况下,this指向构造函数new出来的对象。

3.1 一般情况

1
2
3
4
5
6
7
8
9
10
11
function A() {
this.name = "me";
var age = 15;
console.log(this.name); // me
console.log(this.age); // undefined
}

var a = new A();
console.log(a); // A { name: 'me' }
console.log(a.name); // me
console.log(a.age); // undefined

这也是最常见的过程,类似于定义对象的自有属性。

面试常考题:通过new创建构造函数的一个实例时,发生了什么?

var a = new A()为例:

  1. 创建一个空对象:var obj = {}
  2. 让构造器函数Athis指向对象obj,并执行A中的函数体:var result = A.call(obj)
  3. 设置原型链,让对象obj__proto__属性指向构造器函数A的原型对象:obj.__proto__ = A.prototype
  4. 判断构造器函数A的返回类型,如果是值类型,结果返回obj,如果是引用类型(null除外),返回引用类型的对象:a = result && (typeof result === "object") ? result : obj

如果要手写一个new函数,可能是这样的:

1
2
3
4
5
6
Function.prototype.new = function () {
var obj = {};
var result = this.call(obj);
obj.__proto__ = this.prototype;
return result && (typeof result === "object") ? result : obj;
}

3.2 构造函数当作普通函数使用

毫无疑问,this指向全局对象window这种做法在程序中显然是不推荐的,但做题时要知道this指向谁。

1
2
3
4
5
6
7
8
9
10
11
var name = "var";
var age = 123;

function A() {
this.name = "me"; // 这里把全局对象的name属性修改了
var age = 15;
console.log(this.name); // me
console.log(this.age); // 123
}

A();

3.3 构造函数返回值的情况

一般情况下,构造函数是不需要显式返回值的,通过new执行构造函数的返回值是新生成的实例。

但是,如果构造函数返回一个对象(包括空对象),new执行构造函数的返回值不再是新生成的实例,而是该对象。

构造函数执行过程中,this依然试图指向构造函数的实例,虽然这个实例无法返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
function A() {
this.name = "me";
console.log(this.name); // me
console.log(this.age); // undefined
return {
age: 20
};
}

var a = new A();
console.log(a); // { age: 20 }
console.log(a.name); // undefined
console.log(a.age); // 20

如果构造函数返回的是基本类型、undefinednull时,new执行构造函数的返回值依然是新生成的实例。

1
2
3
4
5
6
7
8
9
10
11
function A() {
this.name = "me";
console.log(this.name); // me
console.log(this.age); // undefined
return null;
}

var a = new A();
console.log(a); // A { name: 'me' }
console.log(a.name); // me
console.log(a.age); // undefined

4 applycallbind调用

三者均可改变this的指向给第一个参数。区别在于:

  • apply的第二个参数为可迭代对象
  • call的参数不固定
  • bind除返回一个函数外,与call并无不同,它需要调用才可以执行,其他二者直接执行函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
age: 15,
fn(x, y) {
console.log(this.age + " " + x + " " + y);
}
}
var obj2 = {
age: 20
}
obj.fn(1, 2); // 15 1 2
obj.fn.apply(obj2, [3, 4]); // 20 3 4
obj.fn.call(obj2, 5, 6); // 20 5 6
obj.fn.call(obj2, [5, 6]); // 20 5,6 undefined
obj.fn.bind(obj2, 7, 8)(); // 20 7 8

注意以上示例12、13行的区别,第13行[5, 6]被当做一个参数传给形参x,形参y得到undefined

5 匿名函数调用

匿名函数不属于任何对象,它的this一般指向全局对象。

5.1 一般情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {
fn() {
return function () {
console.log(this);
}
},
inner: {
fn() {
return function () {
console.log(this);
}
}
}
}
obj.fn()(); // window对象
obj.inner.fn()(); // window对象

无论谁调用、无论如何调用,这里的匿名函数中的this指向全局对象window

5.2 定时器回调

定时器回调函数中,this指向全局对象。因此如果需要改变定时器回调函数中this的指向,需要使用变量提前存储this的指向。

1
2
3
4
5
6
7
8
9
10
var age = 123;
var obj = {
age: 15,
fn() {
setTimeout(function () {
console.log(this.age);
}, 1000);
}
}
obj.fn(); // 123

上述代码中,this指向全局对象window,因此输出全局变量age的值123

1
2
3
4
5
6
7
8
9
10
11
var age = 123;
var obj = {
age: 15,
fn() {
var that = this;
setTimeout(function () {
console.log(that.age);
}, 1000);
}
}
obj.fn(); // 15

上述代码中,使用that提前存储了fnthis的指向,而根据第2节的内容,调用obj.fn()fn中的this指向obj,因此定时器回调函数中能输出objage属性值15

5.3 事件绑定

注意,事件绑定时,this指向事件源。

1
2
3
4
const btn = document.getElementById("btn");
btn.onclick = function () {
console.log(this); // btn对象
}
1
2
3
4
const btn = document.getElementById("btn");
btn.addEventListener("click", function () {
console.log(this); // btn对象
});

如果要清除绑定事件,第一种方法可以通过btn.onclick = null实现,第二种方法可以调用removeEventListener方法。

但是!如果下面这么做,移除是失败的。

1
2
3
4
5
6
7
const btn = document.getElementById("btn");
btn.addEventListener("click", function () {
console.log("点击事件");
});
btn.removeEventListener("click", function () {
console.log("点击事件");
});

创建的两个匿名函数是独立的,彼此没有关系,自然无法被移除。因此,使用addEventListener绑定事件时,建议传入具名的函数变量,而避免使用匿名函数。

1
2
3
4
5
6
7
const btn = document.getElementById("btn");
btn.addEventListener("click", fn);
btn.removeEventListener("click", fn);

function fn() {
console.log("点击事件");
}

6 箭头函数

箭头函数非常特殊,它没有自己的this,而是会寻找定义箭头函数的作用域中this的指向。

箭头函数中的this在其定义时就已确定,与谁调用无关!

1
setTimeout(() => console.log(this), 1000);	// window对象

这个没什么问题。

1
2
3
4
5
6
7
8
var obj = {
fn() {
return function () {
setTimeout(() => console.log(this), 1000);
}
}
}
obj.fn()(); // window

this会寻找定义箭头函数的作用域(即return的匿名函数)中this的指向,因此指向window

1
2
3
4
5
6
var obj = {
fn() {
return () => setTimeout(() => console.log(this), 1000);
}
}
obj.fn()(); // obj

this会寻找定义箭头函数的作用域(即return的箭头函数)中this的指向,由于外层仍是箭头函数,继续向上寻找,直到找到fn作用域中的this且指向为obj

1
2
3
4
5
6
7
8
9
10
let obj = {
name: "me"
}
function fn() {
console.log(this);
return () => console.log(this);
}
fn()(); // window对象 window对象
let fn2 = fn.call(obj); // { name: 'me' }
fn2(); // { name: 'me' }
  • 第8行,fn中的两个this都指向全局对象window,输出两次window对象。
  • 第9行,fn中的thiscall改变指向,此时this指向obj,调用一次fn函数,输出obj{ name: 'me' }
  • 第10行,fn2是一个箭头函数,在第9行中,它的this被改变指向为obj,因此此处输出obj{ name: 'me' }

为什么第10行不是输出window对象?执行第10行时,定义箭头函数的fn作用域中的this应该指向window对象啊。

箭头函数只看它定义时,当前作用域的this指向什么。第6行,箭头函数是在fn函数体内被定义的,因此第9行执行后,在定义箭头函数时,先是把内部的this更改为obj,再返回给fn2。这样fn2调用时(无论如何调用),都输出obj对象。

7 面试题解析

7.1 字节跳动面试题

解释以下代码的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name = "bytedance";
function A() {
console.log(age);
this.name = 123;
var age = 2;
console.log(this.name)
console.log(this.age);
}
A.prototype.getA = function(){
console.log(this.name);
console.log(this);
}
let a = new A();
let funA = a.getA;
funA();

输出为:

1
2
3
4
5
undefined
123
undefined
bytedance
Object [window]
  • 第13行,new一个A的实例,程序开始执行A()
  • 第3行,由于第5行的变量声明var age被提升至函数顶端,因此此处输出undefined
  • 第6行,判断this指向的是a实例,此处输出定义的this.name即123
  • 第7行,由于this指向的a实例没有name属性,因此此处输出undefined
  • 第14行,将a.getA作为一个函数对象赋值给变量funA并在第15行直接调用,程序开始执行getA()
  • 第10行,判断this指向的是全局对象window,因此此处输出全局对象的name属性,而var定义的全局变量为全局对象的属性,即输出"bytedance"
  • 第11行,同上,此处输出全局对象window

扩展:函数对象自带name属性,且值为本身的名称,如A.name返回"A"

Reference

  1. javascript中this的指向问题 - saucxs - 博客园
  2. 彻底理解js中this的指向,不必硬背。 - 追梦子 - 博客园