这this怎么tm这么乱??

如果说 Javascript 中最令人感到困惑的一个概念,this 绝对在其中占有一席之地。可能每个用Javascript的开发人员都遇到过因this指向而引入的 bug,这篇文章致力于梳理清楚这个令人头疼的概念。

[参考文章]

1.https://zh.javascript.info/constructor-new

2.https://juejin.cn/post/6844903680446038023

两个基石

1.JavaScript 中的 this 是在函数被调用时确定的,而不是在函数定义时固定的。

在深入了解 JavaScript 中的 this 关键字之前,有必要先退一步,看一下为什么 this 关键字很重要。this 允许复用函数时使用不同的上下文。换句话说,“this” 关键字允许在调用函数或方法时决定哪个对象应该是焦点。

具体来说,如果我们在一个函数或对象方法的函数体内引入了this关键字,其实我们是希望该函数或方法可以在多种不同情况的上下文中使用,而在不同上下文情况下因为 this 的指向不同进而输出也不相同,通过这种方式,我们就实现了同一个函数或方法在不同的使用环境(上下文)中根据不同的输入(this 指向不同),得到不同的输出,也就是实现了函数的复用。

之后讨论的所有东西都是基于这个理念。我们希望能够在不同的上下文或在不同的对象中复用函数或方法。

因为我们在函数体内引入this 关键字的目的是复用,这也就意味着他们共享着同一个函数定义,自然而然区分不同环境的唯一手段就是在函数调用时区分,简而言之,
JavaScript 中的 this 是在函数被调用时确定的,而不是在函数定义时固定的。

具体 this 的值如何根据调用方式不同而判定,我们下面会进行说明

2.函数是一种特殊的对象,可以被赋值,传递,使用

我们再来将目光聚焦于函数,在Javascript 中,函数是“一等公民”,本质上是一种特殊的对象。这也就意味着函数可以像一般的值(比如数字,字符串,普通对象)那样被赋值,传递,使用。

1
2
3
4
5
6
7
//一个普通的函数
function fun() {
let name = 1;
console.log(this);
}
//查看该函数的值
console.log(fun);

我们可以发现,函数的值就是函数定义本身

image-1

如果是匿名函数呢?

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

image-2

具体例子

我们可以看一些具体例子,

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: "Alice",
greet: function () {
console.log(`Hello, my name is ${this.name}`);
},
};

const person2 = { name: "Bob", greet: person.greet };

person.greet(); // 输出: Hello, my name is Alice
person2.greet(); // 输出: Hello, my name is Bob

person中的greet方法显式的含有this关键字,而person2中的greet方法乍一看好像并没有this关键字,而实际上,根据我们上面两个基石中的第二条,这实际上是将persongreet方法的值赋值给了person2greet方法,本质上person2是这样

1
2
3
4
5
6
const person2 = {
name: 'Bob',
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};

再来看一个例子,首先我想说明,对象里的方法有两种写法,以下两种写法完全等价

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//写法1
let user = {
name: "Bob",
seeThis: function () {
console.log(this);
},
};
//写法2
let user = {
name: "Bob",
seeThis() {
console.log(this);
},
};

//任选上面一种写法,调用user里的seeThis方法
user.seeThis();

运行结果如下

image-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let user = {
name: "Bob",

seeThis() {
console.log(this);
},
// seeThis: () => {
// console.log(this);
// },
};

let tempFun = user.seeThis;
tempFun();
user.seeThis();

image-4

如何根据函数不同调用方式判断 this 的值?

一共有四种不同的调用方式,对应四种不同的绑定 this 值的方式

1.点运算符调用对象方法 obj.func()→ 隐式绑定

注意这种情形最普遍,大约覆盖八成的情形。

如果含 this 的是一个对象的方法,则调用该方法需采用点运算符,则点运算符左边的对象实例就是对象方法里 this 的指向。

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const user = {
name: "Tyler",
age: 27,
greet() {
alert(`Hello, my name is ${this.name}`);
},
};
user.greet(); //
/*
首先判断哪里含this,答案是在user对象的greet方法里含this。即是对象方法里含this,
那在user.greet()语句中用点运算符调用了含this的greet方法,
则其this指向为点运算符左侧的user对象
因此输出Hello, my name is Tyler
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const user = {
name: "Tyler",
age: 27,
greet() {
alert(`Hello, my name is ${this.name}`);
},
mother: {
name: "Stacey",
greet() {
alert(`Hello, my name is ${this.name}`);
},
},
};
user.greet(); // Tyler
user.mother.greet(); // Stacey

2.call,apply,bind 调用函数 → 显式绑定

如果含 this 的不是对象中的方法,而只是一般的函数,那么这时候 this 的值又如何判断呢?

因为此时不能再使用点运算符来进行调用,隐式绑定 this 已经不适用,我们只能显式绑定 this。

即使用 call 来调用函数,同时使用 call 来指定调用时的上下文环境 (this 的值)

1
2
3
4
5
6
7
8
9
function greet() {
alert(`Hello, my name is ${this.name}`);
}

const user = {
name: "Tyler",
age: 27,
};
greet.call(user); //Tyler

如果函数中含有参数,我们怎么在绑定 this 的同时传参呢?其实只需要将参数依次附在绑定的 this 之后即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function greet(lang1, lang2, lang3) {
alert(
`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`
);
}

const user = {
name: "Tyler",
age: 27,
};

const languages = ["JavaScript", "Ruby", "Python"];

greet.call(user, languages[0], languages[1], languages[2]);

但如果参数数量很多的话,将参数一个个传入是很麻烦的,如果可以使用解构赋值就好了(…array),但是 call 不支持结构赋值,但是 apply 支持,我们可以使用 apply 来快速传入多个参数。

1
2
3
4
const languages = ["JavaScript", "Ruby", "Python"];

// greet.call(user, languages[0], languages[1], languages[2])
greet.apply(user, languages);

call 和 apply 方法都会立即调用函数,如果我们不想立即调用,而想返回一个绑定了 this 和参数的函数,在我们需要时在调用,这就用到了 bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function greet(lang1, lang2, lang3) {
alert(
`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`
);
}

const user = {
name: "Tyler",
age: 27,
};

const languages = ["JavaScript", "Ruby", "Python"];

const newFn = greet.bind(user, languages[0], languages[1], languages[2]);
newFn(); // alerts "Hello, my name is Tyler and I know JavaScript, Ruby, and Python"

3.new 操作符调用构造函数→new 绑定

构造函数

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由  "new"  操作符来执行。

例如:

1
2
3
4
5
6
7
8
9
function User(name) {
this.name = name;
this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

当一个函数被使用  new  操作符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给  this
  2. 函数体执行。通常它会修改  this,为其添加新的属性。
  3. 返回  this  的值。

换句话说,new User(...)  做的就是类似的事情:

.

1
2
3
4
5
6
7
8
9
function User(name) {
// this = {};(隐式创建)

// 添加属性到 this
this.name = name;
this.isAdmin = false;

// return this;(隐式返回)
}

所以  new User("Jack")  的结果是相同的对象:

1
2
3
4
let user = {
name: "Jack",
isAdmin: false,
};

现在,如果我们想创建其他用户,我们可以调用  new User("Ann")new User("Alice")  等。比每次都使用字面量创建要短得多,而且更易于阅读。

这是构造器的主要目的 —— 实现可重用的对象创建代码。

让我们再强调一遍 —— 从技术上讲,任何函数(除了箭头函数,它没有自己的  this)都可以用作构造器。即可以通过  new  来运行,它会执行上面的算法。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用  new  来运行。

4.直接调用函数 func()→window 绑定

假如我们有下面这段代码

1
2
3
4
5
6
7
8
9
function sayAge() {
console.log(`My age is ${this.age}`);
}

const user = {
name: "Tyler",
age: 27,
};
sayAge(); // My age is undefined

如前所述,如果你想用 user 做上下文调用 sayAge,你可以使用 .call.apply.bind。但如果我们没有用这些方法,而是直接和平时一样直接调用 sayAge 会发生什么呢?

不出意外,你会得到 My name is undefined,因为 this.age 是 undefined。事情开始变得神奇了。实际上这是因为点的左侧没有任何东西,我们也没有用 .call.apply.bind 或者 new 关键字,JavaScript 会默认 this 指向 window 对象。这意味着如果我们向 window 对象添加 age 属性并再次调用 sayAge 方法,this.age 将不再是 undefined 并且变成 window 对象的 age 属性值。不相信?让我们运行这段代码

1
2
3
4
5
window.age = 27;

function sayAge() {
console.log(`My age is ${this.age}`);
}

非常神奇,不是吗?这就是第 4 条规则为什么是 window 绑定 的原因。如果其它规则都没满足,JavaScript 就会默认 this 指向 window 对象。


在 ES5 添加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 保持为 undefined。

1
2
3
4
5
6
7
'use strict'window.age = 27

function sayAge () {
console.log(`My age is ${this.age}`)
}

sayAge() // TypeError: Cannot read property 'age' of undefined

箭头函数的 this

待施工