深度解析JavaScript的this关键字

这篇文章通过简单的术语和一个真实的例子解释了 this 是什么以及为什么说它很有用。

你的 this

我发现,很多教程在解释 JavaScript 的 this 时,通常会假设你拥有 Java、C++ 或 Python 等面向对象编程语言的背景。这篇文章主要面向那些对 this 没有先入之见的人。我将尝试解释什么是 this 以及为什么它很有用。

或许你迟迟不肯深入探究 this,因为它看起来很奇怪,让你心生畏惧。你之所以使用它,有可能仅仅是因为 StackOverflow 说你需要在 React 用它来完成一些事情。

在我们深入了解它的真正含义以及为什么要使用它之前,我们首先需要了解函数式编程和面向对象编程之间的区别。

函数式编程与面向对象编程

你可能知道也可能不知道,JavaScript 具有函数和面向对象的构造,你可以选择关注其中一个或两者兼而有之。

在我的 JavaScript 之旅的早期,我一方面拥抱函数式编程,一方面像避免瘟疫一样排斥面向对象编程。我对面向对象关键字 this 不甚了解。其中的一个原因是我不明白它存在的必要性。在我看来,完全可以不依赖 this 就可以完成所有的事情。

在某种程度上,我的看法是对的。

你可能只关注其中一种范式而从来不去了解另外一种,作为一名 JavaScript 开发者,你的局限性就体现在这里。为了说明函数式编程和面向对象编程之间的差别,我将使用一组 Facebook 好友数据作为示例。

假设你正在构建一个用户登录 Facebook 的 Web 应用,在登录后显示一些 Facebook 好友的数据。你需要访问 Facebook 端点来获取好友的数据,可能包含一些信息,例如 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts。

const data = [
  {
    firstName: 'Bob',
    lastName: 'Ross',
    username: 'bob.ross',    
    numFriends: 125,
    birthday: '2/23/1985',
    lastTenPosts: ['What a nice day', 'I love Kanye West', ...],
  },
  ...
]

你从(臆造的)Facebook API 获得上面的数据。现在,你需要转换它们,让它们符合项目需要的格式。假设你要为每个用户的朋友显示以下内容:

  • 它们的名字,格式为$ {firstName} $ {lastName}
  • 三篇随机的帖子;
  • 从他们生日起到现在的天数。
函数式方法

如果使用函数式方法,就是将整个数组或数组的每个元素传给一个返回所需操作数据的函数:

const fullNames = getFullNames(data)
// ['Ross, Bob', 'Smith, Joanna', ...]

你从原始数据开始(来自 Facebook API),为了将它们转换为对你有用的数据,你将数据传给一个函数,这个函数将输出你可以在应用程序中显示给用户的数据。

你也可以通过类似的方式获取三个随机帖子并计算朋友生日至今的天数。

函数式方法就是指接受原始数据,将数据传给一个或多个函数,并输出对你有用的数据。

面向对象方法

对于那些刚接触编程和学习 JavaScript 的人来说,面向对象方法可能会更难掌握。面向对象是指你将每个朋友转换为对象,对象包含了用于生成你所需内容的一切。

你可以创建包含 fullName 属性的对象,以及 getThreeRandomPosts 和 getDaysUntilBirthday 函数。

function initializeFriend(data) {
  return {
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from data.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use data.birthday to get the num days until birthday
    }
  };
}
const objectFriends = data.map(initializeFriend)
objectFriends[0].getThreeRandomPosts() 
// Gets three of Bob Ross's posts

面向对象方法是为你的数据创建对象,这些对象包含了状态和用于生成对你和你的项目有用的数据的信息。

这与 this 有什么关系?

你可能没有想过会写出类似 initializeFriend 这样的东西,你可能会认为它很有用。你可能还会注意到,它其实并非真正的面向对象。

getThreeRandomPosts 或 getDaysUntilBirthday 方法之所以有用,主要是因为闭包。因为使用了闭包,所以在 initializeFriend 返回之后,它们仍然可以访问 data。

假设你写了另一个方法,叫 greeting。请注意,在 JavaScript 中,方法只是对象的一个属性,这个属性的值是一个函数。我们希望 greeting 可以做这些事情:

function initializeFriend(data) {
  return {
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from data.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use data.birthday to get the num days until birthday
    },
    greeting: function() {
      return `Hello, this is ${fullName}'s data!`
    }
  };
}

这样可以吗?

不行!

新创建对象的所有东西都可以访问 initializeFriend 的变量,但对象本身的属性或方法不行。当然,你可能会问:

难道你不能用 data.firstName 和 data.lastName 来返回 greeting 吗?

当然可以。但如果我们还想在 greeting 中包含朋友生日至今的天数,该怎么办?我们必须以某种方式从 greeting 中调用 getDaysUntilBirthday 方法。

是时候让 this 上场了!

那么,this 是什么

在不同的情况下,this 代表的东西也不一样。默认情况下,this 指向全局对象(在浏览器中,就是 window 对象)。但光知道这点对我们并没有太大帮助,对我来说有用的是 this 的这条规则:

如果 this 被用在一个对象的方法中,并且这个方法在对象的上下文中调用,那么 this 就指向这个对象本身。

你会问:“在对象的上下文中调用……这又是什么意思”?

别担心,稍后我们会解释这个。

因此,如果我们想在 greeting 中调用 getDaysUntilBirthday,可以直接调用 this.getDaysUntilBirthday,因为在这种情况下,this 指向对象本身。

注意:不要在全局作用域或在另一个函数作用域内的常规 ole 函数中使用 this!this 是一个面向对象的构造。因此,它只在对象(或类)的上下文中有意义!

让我们重构 initializeFriend,让它使用 this:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const numDays = this.getDaysUntilBirthday()      
      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`
    }
  };
}

现在,在执行完 intializeFriend 后,这个对象的所有东西都限定在对象本身。我们的方法不再依赖于闭包,它们将使用对象本身包含的信息。

这是 this 的一种使用方式,现在回到之前的问题:为什么说 this 因上下文不同而不同?

有时候,你希望 this 可以指向不一样的东西,比如事件处理程序就是一个很好的例子。假设我们想在用户点击链接时打开朋友的 Facebook 页面。我们可能会在对象中添加一个 onClick 方法:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const numDays = this.getDaysUntilBirthday()      
      return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

请注意,我们向对象添加了 username,让 onFriendClick 可以访问它,这样我们就可以在新窗口中打开朋友的 Facebook 页面。现在编写 HTML:

<button id="Bob_Ross">
  <!-- A bunch of info associated with Bob Ross -->
</button> 

然后是 JavaScript:

const bobRossObj = initializeFriend(data[0])
const bobRossDOMEl = document.getElementById('Bob_Ross')
bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

在上面的代码中,我们为 Bob Ross 创建了一个对象。我们获得与 Bob Ross 相关的 DOM 元素。现在我们想要调用 onFriendClick 方法来打开 Bob 的 Facebook 页面。应该没问题吧?

不行!

什么地方出了问题?

请注意,我们为 onclick 处理程序选择的函数是 bobRossObj.onFriendClick。看到问题所在了吗?如果我们像这样重写它:

bobRossDOMEl.addEventListener("onclick", function() {
  window.open(`https://facebook.com/${this.username}`)
})

现在你看到问题所在了吗?当我们将 onclick 处理程序指定为 bobRossObj.onFriendClick 时,我们实际上是将 bobRossObj.onFriendClick 的函数作为参数传给了处理程序。它不再“属于”bobRossObj,也就是说 this 不再指向 bobRossObj。这个时候 this 实际上指向的是全局对象,所以 this.username 是 undefined 的。

是时候让 bind 上场了!

显式绑定 this

我们需要做的是将 this 显式绑定到 bobRossObj。我们可以使用 bind 来实现:

const bobRossObj = initializeFriend(data[0])
const bobRossDOMEl = document.getElementById('Bob_Ross')
bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj)
bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)

之前,this 是基于默认规则设置的。通过使用 bind,我们在 bobRossObj.onFriendClick 中将 this 的值显式设置为对象本身,也就是 bobRossObj。

到目前为止,我们已经知道为什么 this 很有用以及为什么有时候需要显式绑定 this。接下来我们要讨论的最后一个主题是箭头函数。

箭头函数

你可能已经注意到,箭头函数像是一个时髦的新事物。人们似乎很喜欢它们,因为它们简洁而优雅。你可能已经知道它们与一般函数略有不同,但不一定非常清楚这些区别究竟是什么。

或许箭头函数的不同之处在于:

在箭头函数内部,无论 this 处于什么位置,它指的都是相同的东西。

让我们用 initializeFriend 示例解释一下。假设我们想在 greeting 中添加一个辅助函数:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      function getLastPost() {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

这样可以吗?如果不行,要怎样修改才行?

这样当然是不行的。因为 getLastPost 不是在对象的上下文中调用的,所以 getLastPost 中的 this 会回退到默认规则,即指向全局对象。

“在对象的上下文中调用”可能是一个比较含糊的概念。要确定一个函数是否是在“对象的上下文中”被调用,最好的办法是看一下函数是如何被调用的,以及是否有对象“附加”在函数上。

让我们来看看执行 bobRossObj.onFriendClick() 时会发生什么:“找到 bobRossObj 对象的 onFriendClick 属性,调用分配给这个属性的函数”。

再让我们来看看执行 getLastPost() 时会发生什么:”调用一个叫作 getLastPost 的函数”。有没有注意到,这里并没有提及任何对象?

现在来测试一下。假设有一个叫作 functionCaller 的函数,它所做的事情就是调用其他函数:

functionCaller(fn) {
  fn()
}

如果我们这样做会怎样:functionCaller(bobRossObj.onFriendClick)?可不可以说 onFriendClick 是“在对象的上下文中”被调用的?this.username 的定义存在吗?

让我们来看一下:“找到 bobRossObj 对象的 onFriendClick 属性。找到这个属性的值(恰好是一个函数),将它传给 functionCaller,并命名为 fn。现在,执行名为 fn 的函数”。请注意,函数在被调用之前已经从 bobRossObj 对象中“分离”,因此不是“在对象 bobRossObj 的上下文中”调用,所以 this.username 是 undefined 的。

让箭头函数来救场:

function initializeFriend(data) {
  return {
    lastTenPosts: data.lastTenPosts,
    birthday: data.birthday,
    username: data.username,    
    fullName: `${data.firstName} ${data.lastName}`,
    getThreeRandomPosts: function() {
      // get three random posts from this.lastTenPosts
    },
    getDaysUntilBirthday: function() {
      // use this.birthday to get the num days until birthday
    },
    greeting: function() {
      const getLastPost = () => {
        return this.lastTenPosts[0]
      }
      const lastPost = getLastPost()           
      return `Hello, this is ${this.fullName}'s data!
             ${this.fullName}'s last post was ${lastPost}.`
    },
    onFriendClick: function() {
      window.open(`https://facebook.com/${this.username}`)
    }
  };
}

箭头函数是在 greeting 中声明的。我们知道,当我们在 greeting 中使用 this 时,它指向对象本身。因此,箭头函数中的 this 指向的对象就是我们想要的。

英文原文:

https://medium.freecodecamp.org/a-deep-dive-into-this-in-javascript-why-its-critical-to-writing-good-code-7dca7eb489e7

发表评论

电子邮件地址不会被公开。 必填项已用*标注