JavaScript 的这个难点,毁掉了多少程序员?

2022-10-1922:08:10编程语言入门到精通Comments832 views字数 9105阅读模式

this 适合你吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我看到许多文章在介绍 JavaScript 的 this 时都会假设你学过某种面向对象的编程语言,比如 Java、C++ 或 Python 等。但这篇文章面向的读者是那些不知道 this 是什么的人。我尽量不用任何术语来解释 this 是什么,以及 this 的用法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

也许你一直不敢解开 this 的秘密,因为它看起来挺奇怪也挺吓人的。或许你只在 StackOverflow 说你需要用它的时候(比如在 React 里实现某个功能)才会使用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

在深入介绍 this 之前,我们首先需要理解函数式编程和面向对象编程之间的区别。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

函数式编程 vs 面向对象编程文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

你可能不知道,JavaScript 同时拥有面向对象和函数式的结构,所以你可以自己选择用哪种风格,或者两者都用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我在很早以前使用 JavaScript 时就喜欢函数式编程,而且会像躲避瘟疫一样避开面向对象编程,因为我不理解面向对象中的关键字,比如 this。我不知道为什么要用 this。似乎没有它我也可以做好所有的工作。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

而且我是对的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

在某种意义上 。也许你可以只专注于一种结构并且完全忽略另一种,但这样你只能是一个 JavaScript 开发者。为了解释函数式和面向对象之间的区别,下面我们通过一个数组来举例说明,数组的内容是 Facebook 的好友列表。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

假设你要做一个 Web 应用,当用户使用 Facebook 登录你的 Web 应用时,需要显示他们的 Facebook 的好友信息。你需要访问 Facebook 并获得用户的好友数据。这些数据可能是 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts 等信息。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

const data = [  {    firstName: Bob,    lastName: Ross,    username: bob.ross,        numFriends: 125,    birthday: 2/23/1985,    lastTenPosts: [What a nice day, I love Kanye West, ...],  },  ...]文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

假设上述数据是你通过 Facebook API 获得的。现在需要将其转换成方便你的项目使用的格式。我们假设你想显示的好友信息如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • 姓名,格式为`${firstName} ${lastName}`文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • 三篇随机文章文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • 距离生日的天数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

函数式方式文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

函数式的方式就是将整个数组或者数组中的某个元素传递给某个函数,然后返回你需要的信息:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

const fullNames = getFullNames(data)// [Ross, Bob, Smith, Joanna, ...]文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

首先我们有 Facebook API 返回的原始数据。为了将其转换成需要的格式,首先要将数据传递给一个函数,函数的输出是(或者包含)经过修改的数据,这些数据可以在应用中向用户展示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们可以用类似的方法获得随机三篇文章,并且计算距离好友生日的天数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

函数式的方式是:将原始数据传递给一个函数或者多个函数,获得对你的项目有用的数据格式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

面向对象的方式文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

对于编程初学者和 JavaScript 初学者,面向对象的概念可能有点难以理解。其思想是,我们要将每个好友变成一个对象,这个对象能够生成你一切开发者需要的东西。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

你可以创建一个对象,这个对象对应于某个好友,它有 fullName 属性,还有两个函数 getThreeRandomPosts 和 getDaysUntilBirthday。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

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 Rosss posts文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

面向对象的方式就是为数据创建对象,每个对象都有自己的状态,并且包含必要的信息,能够生成需要的数据。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

这跟 this 有什么关系?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

你也许从来没想过要写上面的 initializeFriend 代码,而且你也许认为,这种代码可能会很有用。但你也注意到,这并不是真正的面向对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

其原因就是,上面例子中的 getThreeRandomPosts 或 getdaysUntilBirtyday 能够正常工作的原因其实是闭包。因为使用了闭包,它们在 initializeFriend 返回之后依然能访问 data。关于闭包的更多信息可以看看这篇文章:作用域和闭包(https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch5.md)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

还有一个方法该怎么处理?我们假设这个方法叫做 greeting。注意方法(与 JavaScript 的对象有关的方法)其实只是一个属性,只不过属性值是函数而已。我们想在 greeting 中实现以下功能:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

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!`    }  };}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

这样能正常工作吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

不能!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们新建的对象能够访问 initializeFriend 中的一切变量,但不能访问这个对象本身的属性或方法。当然你会问,文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

难道不能在 greeting 中直接用 data.firstName 和 data.lastName 吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

当然可以。但要是想在 greeting 中加入距离好友生日的天数怎么办?我们最好还是有办法在 greeting 中调用 getDaysUntilBirthday。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

这时轮到 this 出场了!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

终于——this 是什么文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

this 在不同的环境中可以指代不同的东西。默认的全局环境中 this 指代的是全局对象(在浏览器中 this 是 window 对象),这没什么太大的用途。而在 this 的规则中具有实用性的是这一条:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

如果在对象的方法中使用 this,而该方法在该对象的上下文中调用,那么 this 指代该对象本身。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

你会说“在该对象的上下文中调用”……是啥意思?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

别着急,我们一会儿就说。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

所以,如果我们想从 greeting 中调用 getDaysUntilBirtyday 我们只需要写 this.getDaysUntilBirthday,因为此时的 this 就是对象本身。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

附注:不要在全局作用域的普通函数或另一个函数的作用域中使用 this!this 是个面向对象的东西,它只在对象的上下文(或类的上下文)中有意义。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们利用 this 来重写 initializeFriend:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

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!`    }  };}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

现在,在 initializeFriend 执行结束后,该对象需要的一切都位于对象本身的作用域之内了。我们的方法不需要再依赖于闭包,它们只会用到对象本身包含的信息。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

好吧,这是 this 的用法之一,但你说过 this 在不同的上下文中有不同的含义。那是什么意思?为什么不一定会指向对象自己?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

有时候,你需要将 this 指向某个特定的东西。一种情况就是事件处理函数。比如我们希望在用户点击好友时打开好友的 Facebook 首页。我们会给对象添加下面的 onClick 方法:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

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}`)    }  };}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

注意我们在对象中添加了 username 属性,这样 onFriendClick 就能访问它,从而在新窗口中打开该好友的 Facebook 首页。现在只需要编写 HTML:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

<button id="Bob_Ross">  <!-- A bunch of info associated with Bob Ross --></button> 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

还有 JavaScript:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

const bobRossObj = initializeFriend(data[0])const bobRossDOMEl = document.getElementById(Bob_Ross)bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

在上述代码中,我们给 Bob Ross 创建了一个对象。然后我们拿到了 Bob Ross 对应的 DOM 元素。然后执行 onFriendClick 方法来打开 Bob 的 Facebook 主页。似乎没问题,对吧?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

有问题!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

哪里出错了?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

注意我们调用 onclick 处理程序的代码是 bobRossObj.onFriendClick。看到问题了吗?要是写成这样的话能看出来吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

bobRossDOMEl.addEventListener("onclick", function() {  window.open(`https://facebook.com/${this.username}`)})文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

现在看到问题了吗?如果把事件处理程序写成 bobRossObj.onFriendClick,实际上是把 bobRossObj.onFriendClick 上保存的函数拿出来,然后作为参数传递。它不再“依附”在 bobRossObj 上,也就是说,this 不再指向 bobRossObj。它实际指向全局对象,也就是说 this.username 不存在。似乎我们没什么办法了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

轮到绑定上场了!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

明确绑定 this文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们需要明确地将 this 绑定到 bobRossObj 上。我们可以通过 bind 实现:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

const bobRossObj = initializeFriend(data[0])const bobRossDOMEl = document.getElementById(Bob_Ross)bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj)bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

之前,this 是按照默认的规则设置的。但使用 bind 之后,我们明确地将 bobRossObj.onFriendClick 中的 this 的值设置为 bobRossObj 对象本身。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

到此为止,我们看到了为什么要使用 this,以及为什么要明确地绑定 this。最后我们来介绍一下,this 实际上是箭头函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

箭头函数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

你也许注意到了箭头函数最近很流行。人们喜欢箭头函数,因为很简洁、很优雅。而且你还知道箭头函数和普通函数有点区别,尽管不太清楚具体区别是什么。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

简而言之,两者的区别在于:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

在定义箭头函数时,不管 this 指向谁,箭头函数内部的 this 永远指向同一个东西。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

嗯……这貌似没什么用……似乎跟普通函数的行为一样啊?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们通过 initializeFriend 举例说明。假设我们想添加一个名为 greeting 的函数:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

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}`)    }  };}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

这样能运行吗?如果不能,怎样修改才能运行?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

答案是不能。因为 getLastPost 没有在对象的上下文中调用,因此getLastPost 中的 this 按照默认规则指向了全局对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

你说没有“在对象的上下文中调用”……难道它不是从 initializeFriend 返回的内部调用的吗?如果这还不叫“在对象的上下文中调用”,那我就不知道什么才算了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我知道“在对象的上下文中调用”这个术语很模糊。也许,判断函数是否“在对象的上下文中调用”的好方法就是检查一遍函数的调用过程,看看是否有个对象“依附”到了函数上。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们来检查下执行 bobRossObj.onFriendClick() 时的情况。“给我对象 bobRossObj,找到其中的 onFriendClick 然后调用该属性对应的函数”。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们同样检查下执行 getLastPost() 时的情况。“给我名为 getLastPost 的函数然后执行。”看到了吗?我们根本没有提到对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

好了,这里有个难题来测试你的理解程度。假设有个函数名为 functionCaller,它的功能就是调用一个函数:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

functionCaller(fn) {  fn()}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

如果调用 functionCaller(bobRossObj.onFriendClick) 会怎样?你会认为 onFriendClick 是“在对象的上下文中调用”的吗?this.username有定义吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我们来检查一遍:“给我 bobRosObj 对象然后查找其属性 onFriendClick。取出其中的值(这个值碰巧是个函数),然后将它传递给 functionCaller,取名为 fn。然后,执行名为 fn 的函数。”注意该函数在调用之前已经从 bobRossObj 对象上“脱离”了,因此并不是“在对象的上下文中调用”的,所以 this.username 没有定义。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

这时可以用箭头函数解决这个问题:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

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}`)    }  };}文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

上述代码的规则是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

在定义箭头函数时,不管 this 指向谁,箭头函数内部的 this 永远指向同一个东西。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

箭头函数是在 greeting 中定义的。我们知道,在 greeting 内部的 this 指向对象本身。因此,箭头函数内部的 this 也指向对象本身,这正是我们需要的结果。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

结论文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

this 有时很不好理解,但它对于开发 JavaScript 应用非常有用。本文当然没能介绍 this 的所有方面。一些没有涉及到的话题包括:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • call 和 apply; 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • 使用 new 时 this 会怎样;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • 在 ES6 的 class 中 this 会怎样。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

我建议你首先问问自己在这些情况下的 this,然后在浏览器中执行代码来检验你的结果。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

想学习更多关 于this 的内容,可参考《你不知道的 JS:this 和对象原型》:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

如果你想测试自己的知识,可参考《你不知道的JS练习:this和对象原型》:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

  • https://ydkjs-exercises.com/this-object-prototypes文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

原文:https://medium.freecodecamp.org/a-deep-dive-into-this-in-javascript-why-its-critical-to-writing-good-code-7dca7eb489e7文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

作者:Austin Tackaberry,Human API 的软件工程师文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

译者:弯月,责编:屠敏文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/28557.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/ymba/28557.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定