如果说 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 | //一个普通的函数 |
我们可以发现,函数的值就是函数定义本身
如果是匿名函数呢?
1 | console.log(function() { |
具体例子
我们可以看一些具体例子,
1 | const person = { |
person
中的greet
方法显式的含有this
关键字,而person2
中的greet
方法乍一看好像并没有this
关键字,而实际上,根据我们上面两个基石中的第二条,这实际上是将person
的greet
方法的值赋值给了person2
的greet
方法,本质上person2
是这样
1 | const person2 = { |
再来看一个例子,首先我想说明,对象里的方法有两种写法,以下两种写法完全等价
1 | //写法1 |
运行结果如下
1 | let user = { |
如何根据函数不同调用方式判断 this 的值?
一共有四种不同的调用方式,对应四种不同的绑定 this 值的方式
1.点运算符调用对象方法 obj.func()→ 隐式绑定
注意这种情形最普遍,大约覆盖八成的情形。
如果含 this 的是一个对象的方法,则调用该方法需采用点运算符,则点运算符左边的对象实例就是对象方法里 this 的指向。
举例
1 | const user = { |
1 | const user = { |
2.call,apply,bind 调用函数 → 显式绑定
如果含 this 的不是对象中的方法,而只是一般的函数,那么这时候 this 的值又如何判断呢?
因为此时不能再使用点运算符来进行调用,隐式绑定 this 已经不适用,我们只能显式绑定 this。
即使用 call 来调用函数,同时使用 call 来指定调用时的上下文环境 (this 的值)
1 | function greet() { |
如果函数中含有参数,我们怎么在绑定 this 的同时传参呢?其实只需要将参数依次附在绑定的 this 之后即可。
1 | function greet(lang1, lang2, lang3) { |
但如果参数数量很多的话,将参数一个个传入是很麻烦的,如果可以使用解构赋值就好了(…array
),但是 call 不支持结构赋值,但是 apply 支持,我们可以使用 apply 来快速传入多个参数。
1 | const languages = ["JavaScript", "Ruby", "Python"]; |
call 和 apply 方法都会立即调用函数,如果我们不想立即调用,而想返回一个绑定了 this 和参数的函数,在我们需要时在调用,这就用到了 bind
1 | function greet(lang1, lang2, lang3) { |
3.new 操作符调用构造函数→new 绑定
构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由
"new"
操作符来执行。
例如:
1 | function User(name) { |
当一个函数被使用 new
操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this
。 - 函数体执行。通常它会修改
this
,为其添加新的属性。 - 返回
this
的值。
换句话说,new User(...)
做的就是类似的事情:
.
1 | function User(name) { |
所以 new User("Jack")
的结果是相同的对象:
1 | let user = { |
现在,如果我们想创建其他用户,我们可以调用 new User("Ann")
,new User("Alice")
等。比每次都使用字面量创建要短得多,而且更易于阅读。
这是构造器的主要目的 —— 实现可重用的对象创建代码。
让我们再强调一遍 —— 从技术上讲,任何函数(除了箭头函数,它没有自己的 this
)都可以用作构造器。即可以通过 new
来运行,它会执行上面的算法。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new
来运行。
4.直接调用函数 func()→window 绑定
假如我们有下面这段代码
1 | function sayAge() { |
如前所述,如果你想用 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 | window.age = 27; |
非常神奇,不是吗?这就是第 4 条规则为什么是 window 绑定
的原因。如果其它规则都没满足,JavaScript 就会默认 this
指向 window
对象。
在 ES5 添加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 保持为 undefined。
1 | window.age = 27 |
箭头函数的 this
待施工