this 是一个令无数 JavaScript 编程者又爱又恨的知识点。它的重要性毋庸置疑,然而真正想掌握它却并非易事。希望本文可以帮助大家理解 this。
JavaScript 中的 this
JavaScript 引擎在查找 this 时不会通过原型链一层一层的查找,因为 this 完全是在函数调用时才可以确定的,让我们来看下面几种函数调用的形式。
1. Function Invocation Pattern
普通的函数调用,这是我们使用较多的一种, foo 是以单独的变量出现而不是属性。其中的 this 指向全局对象。
- function foo() {
- console.log(this)
- }
- foo() // Window
函数作为对象的方法调用,会通过 obj.func 或者 obj[func] 的形式调用。其中的 this 指向调用它的对象。
- const obj = {
- name: 'lxfriday',
- getName(){
- console.log(this.name)
- }
- }
- obj.getName() // lxfriday
2. Constructor Pattern
通过 new Constructor() 的形式调用,其 this 会指向新生成的对象。
- function Person(name){
- this.name = name
- }
- const person = new Person('lxfriday')
- console.log(person.name) // lxfriday
3. Apply Pattern
通过 foo.apply(thisObj) 或者 foo.call(thisObj) 的形式调用,其中的 this 指向 thisObj。如果 thisObj 是 null 或者 undefined ,其中的 this 会指向全局上下文 Window(在浏览器中)。
掌握以上的几种函数调用形式就基本可以覆盖开发中遇到的常见问题了,下面我翻译了一篇文章,帮助你更深入的理解 this。
如果你已经使用过一些 JavaScript 库,你一定会注意到一个特殊的关键字 this。
this 在 JavaScript 中很常见,但是有很多开发人员花了很多时间来完全理解 this 关键字的确切功能以及在代码中何处使用。
在这篇文章中,我将帮助您深入了解 this 其机制。
在深入了解之前,请确保已在系统上安装了 Node 。然后,打开命令终端并运行 node 命令。
全局环境中的 this
this 的工作机制并不容易理解。为了理解 this 是如何工作的,我们将探索不同环境中的 this。首先我们从全局上下文开始。
在全局层面中,this 等同于全局对象,在 Node repl(交互式命令行) 环境中叫 global。
- $ node
- > this === global
- true
但上述情况只出现在 Node repl 环境中,如果我们在 JS 文件中跑相同的代码,我们将会得到不同的答案。
为了测试,我们创建一个 index.js 的文件,并添加下面的代码:
- console.log(this === global);
然后通过 node 命令运行:
- $ node index.js
- false
出现上面情况的原因是在 JS 文件中, this 指向 module.exports,并不是指向 global。
函数中的 this
Function Invocation Pattern
在函数中 this 的指向取决于函数的调用形式。所以,函数每次执行的时候,可能拥有不同的 this 指向。
在 index.js 文件中,编写一个非常简单的函数来检查 this 是否指向全局对象:
- function fat() {
- console.log(this === global)
- }
- fat()
如果我们在 Node repl 环境执行上面的代码,将会得到 true,但是如果添加 use strict 到首行,将会得到 false,因为这个时候 this 的值为 undefined。
为了进一步说明这一点,让我们创建一个定义超级英雄的真实姓名和英雄姓名的简单函数。
- function Hero(heroName, realName) {
- this.realName = realName;
- this.heroName = heroName;
- }
- const superman= Hero("Superman", "Clark Kent");
- console.log(superman);
请注意,这个函数不是在严格模式下执行的。代码在 node 中运行将不会出现我们预期的 Superman 和 Clark Kent ,我们将得到 undefined。
这背后的原因是由于该函数不是以严格模式编写的,所以 this 引用了全局对象。
如果我们在严格模式下运行这段代码,会因为 JavaScript 不允许给 undefined 增加属性而出现错误。这实际上是一件好事,因为它阻止我们创建全局变量。
最后,以大写形式编写函数的名称意味着我们需要使用 new 运算符将其作为构造函数来调用。将上面的代码片段的最后两行替换为:
- const superman = new Hero("Superman", "Clark Kent");
- console.log(superman);
再次运行 node index.js 命令,您现在将获得预期的输出。
构造函数中的 this
Constructor Pattern
JavaScript 没有任何特殊的构造函数。我们所能做的就是使用 new 运算符将函数调用转换为构造函数调用,如上一节所示。
进行构造函数调用时,将创建一个新对象并将其设置为函数的 this 参数。然后,从函数隐式返回该对象,除非我们有另一个要显式返回的对象。
在 hero 函数内部编写以下 return 语句:
- return {
- heroName: "Batman",
- realName: "Bruce Wayne",
- };
如果现在运行 node 命令,我们将看到 return 语句将覆盖构造函数调用。
当 return 语句尝试返回不是对象的任何东西时,将隐式返回 this。
方法中的 this
Method Invocation Pattern
当将函数作为对象的方法调用时,this 指向该对象,然后将该对象称为该函数调用的接收者。
在下面代码中,有一个 dialogue 方法在 hero 对象内。通过 hero.dialogue() 形式调用时,dialogue 中的 this 就会指向 hero 本身。这里,hero 就是 dialogue 方法调用的接收者。
- const hero = {
- heroName: "Batman",
- dialogue() {
- console.log(`I am ${this.heroName}!`);
- }
- };
- hero.dialogue();
上面的代码非常简单,但是实际开发时有可能方法调用的接收者并不是原对象。看下面的代码:
- const saying = hero.dialogue();
- saying();
这里,我们把方法赋值给一个变量,然后执行这个变量指向的函数,你会发现 this 的值是 undefined。这是因为 dialogue 方法已经无法跟踪原来的接收者对象,函数现在指向的是全局对象。
当我们将一个方法作为回调传递给另一个方法时,通常会发生接收器的丢失。我们可以通过添加包装函数或使用 bind 方法将 this 绑定到特定对象来解决此问题。
call、apply
Apply Pattern
尽管函数的 this 值是隐式设置的,但我们也可以通过 call()和 apply() 显式地绑定 this。
让我们像这样重组前面的代码片段:
- function dialogue () {
- console.log (`I am ${this.heroName}`);
- }
- const hero = {
- heroName: 'Batman',
- };
我们需要将hero 对象作为接收器与 dialogue 函数连接。为此,我们可以使用 call() 或 apply() 来实现连接:
- dialogue.call(hero)
- // or
- dialogue.apply(hero)
需要注意的是,在非严格模式下,如果传递 null 或者 undefined 给 call 、 apply 作为上下文,将会导致 this 指向全局对象。
- function dialogue() {
- console.log('this', this)
- }
- const hero = {
- heroName: 'Batman',
- }
- console.log(dialogue.call(null))
上述代码,在严格模式下输出 null,非严格模式下输出全局对象。
bind
当我们将一个方法作为回调传递给另一个函数时,始终存在丢失该方法的预期接收者的风险,导致将 this 参数设置为全局对象。
bind() 方法允许我们将 this 参数永久绑定到函数。因此,在下面的代码片段中,bind 将创建一个新 dialogue 函数并将其 this 值设置为 hero。
- const hero = {
- heroName: "Batman",
- dialogue() {
- console.log(`I am ${this.heroName}`);
- }
- };
- // 1s 后打印:I am Batman
- setTimeout(hero.dialogue.bind(hero), 1000);
注意:对于用 bind 绑定 this 之后新生成的函数,使用 call 或者 apply 方法无法更改这个新函数的 this。
箭头函数中的 this
箭头函数和普通函数有很大的不同,引用阮一峰 ES6入门第六章中的介绍:
- 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;
- 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误;
- 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替;
- 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数;
上面四点中,第一点尤其值得注意。this 对象的指向是可变的,但是在箭头函数中,它是固定的,它只指向箭头函数定义时的外层 this,箭头函数没有自己的 this,所有绑定 this 的操作,如 call apply bind 等,对箭头函数中的 this 绑定都是无效的。
让们看下面的代码:
- const batman = this;
- const bruce = () => {
- console.log(this === batman);
- };
- bruce();
在这里,我们将 this 的值存储在变量中,然后将该值与箭头函数内部的 this 值进行比较。node index.js 执行时将会输出 true。
那箭头函数中的 this 可以做哪些事情呢?
箭头函数可以帮助我们在回调中访问 this。看一下我在下面写的 counter 对象:
- const counter = {
- count: 0,
- increase() {
- setInterval(function() {
- console.log(++this.count);
- }, 1000);
- }
- }
- counter.increase();
运行上面的代码,会打印 NaN。这是因为 this.count 没有指向 counter 对象。它实际上指向全局对象。
要使此计数器工作,可以用箭头函数重写,下面代码将会正常运行:
- const counter = {
- count: 0,
- increase () {
- setInterval (() => {
- console.log (++this.count);
- }, 1000);
- },
- };
- counter.increase ();
类中的 this
类是所有 JavaScript 应用程序中最重要的部分之一。让我们看看类内部 this 的行为。
一个类通常包含一个 constructor,其中 this 将指向新创建的对象。
但是,在使用方法的情况下,如果该方法以普通函数的形式调用,则 this 也可以指向任何其他值。就像一个方法一样,类也可能无法跟踪接收者。
我们用类重写上面的 Hero 函数。此类将包含构造函数和 dialogue() 方法。最后,我们创建此类的实例并调用该 dialogue 方法。
- class Hero {
- constructor(heroName) {
- this.heroName = heroName;
- }
- dialogue() {
- console.log(`I am ${this.heroName}`)
- }
- }
- const batman = new Hero("Batman");
- batman.dialogue();
constructor 中的 this 指向新创建的类实例。batman.dialogue() 调用时,我们将 dialogue() 作为 batman 接收器的方法调用。
但是,如果我们存储对 dialogue() 方法的引用,然后将其作为函数调用,则我们将再次失去方法的接收者,而 this 现在指向 undefined。
为什么是指向 undefined 呢?这是因为 JavaScript 类内部隐式以严格模式运行。我们将 say() 作为一个函数调用而没有进行绑定。所以我们要手动的绑定。
- const say = batman.dialogue.bind(batman);
- say();
当然,我们也可以在构造函数内部绑定:
- class Hero {
- constructor(heroName) {
- this.heroName = heroName
- thisthis.dialogue = this.dialogue.bind(this)
- }
- dialogue() {
- console.log(`I am ${this.heroName}`)
- }
- }
加餐:手写 call、apply、bind
call 和 apply 的模拟实现大同小异,注意 apply 的参数是一个数组,绑定 this 都采用的是对象调用方法的形式。
- Function.prototype.call = function(thisObj) {
- thisObjthisObj = thisObj || window
- const funcName = Symbol('func')
- const that = this // func
- thisObj[funcName] = that
- const result = thisObj[funcName](...arguments)
- delete thisObj[funcName]
- return result
- }
- Function.prototype.apply = function(thisObj) {
- thisObjthisObj = thisObj || window
- const funcName = Symbol('func')
- const that = this // func
- const args = arguments[1] || []
- thisObj[funcName] = that
- const result = thisObj[funcName](...[thisObj, ...args])
- delete thisObj[funcName]
- return result
- }
- Function.prototype.bind = function(thisObj) {
- thisObjthisObj = thisObj || window
- const that = this // func
- const outerArgs = [...arguments].slice(1)
- return function(...innerArgs) {
- return that.apply(thisObj, outerArgs.concat(innerArgs))
- }
- }