【CSDN编者按】本文将通过简单的术语和真实世界的例子解释 JavaScript 中 this 及其用途,并告诉你写出好的代码为何如此重要。

this 适合你吗?

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

也许你一直不敢解开 this 的秘密,因为它看起来挺奇怪也挺吓人的。或许你只在 StackOverflow 说你需要用它的时候(比如在 React 里实现某个功能)才会使用。

在深入介绍 this 之前,我们首先需要理解函数式编程和面向对象编程之间的区别。

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

你可能不知道,JavaScript 同时拥有面向对象和函数式的结构,所以你可以自己选择用哪种风格,或者两者都用。

我在很早以前使用 JavaScript 时就喜欢函数式编程,而且会像躲避瘟疫一样避开面向对象编程,因为我不理解面向对象中的关键字,比如 this。我不知道为什么要用 this。似乎没有它我也可以做好所有的工作。

而且我是对的。

在某种意义上 。也许你可以只专注于一种结构并且完全忽略另一种,但这样你只能是一个 JavaScript 开发者。为了解释函数式和面向对象之间的区别,下面我们通过一个数组来举例说明,数组的内容是 Facebook 的好友列表。

假设你要做一个 Web 应用,当用户使用 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 或 getdaysUntilBirtyday 能够正常工作的原因其实是闭包。因为使用了闭包,它们在 initializeFriend 返回之后依然能访问 data。关于闭包的更多信息可以看看这篇文章:作用域和闭包(https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch5.md)。

还有一个方法该怎么处理?我们假设这个方法叫做 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 中的一切变量,但不能访问这个对象本身的属性或方法。当然你会问,

难道不能在 greeting 中直接用 data.firstName 和 data.lastName 吗?

当然可以。但要是想在 greeting 中加入距离好友生日的天数怎么办?我们最好还是有办法在 greeting 中调用 getDaysUntilBirthday。

这时轮到 this 出场了!

终于——this 是什么

this 在不同的环境中可以指代不同的东西。默认的全局环境中 this 指代的是全局对象(在浏览器中 this 是 window 对象),这没什么太大的用途。而在 this 的规则中具有实用性的是这一条:

如果在对象的方法中使用 this,而该方法在该对象的上下文中调用,那么 this 指代该对象本身。

你会说“在该对象的上下文中调用”……是啥意思?

别着急,我们一会儿就说。

所以,如果我们想从 greeting 中调用 getDaysUntilBirtyday 我们只需要写 this.getDaysUntilBirthday,因为此时的 this 就是对象本身。

附注:不要在全局作用域的普通函数或另一个函数的作用域中使用 this!this 是个面向对象的东西,它只在对象的上下文(或类的上下文)中有意义。

我们利用 this 来重写 initializeFriend:

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!`
    }
  };
}

现在,在 initializeFriend 执行结束后,该对象需要的一切都位于对象本身的作用域之内了。我们的方法不需要再依赖于闭包,它们只会用到对象本身包含的信息。

好吧,这是 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}`)
})

现在看到问题了吗?如果把事件处理程序写成 bobRossObj.onFriendClick,实际上是把 bobRossObj.onFriendClick 上保存的函数拿出来,然后作为参数传递。它不再“依附”在 bobRossObj 上,也就是说,this 不再指向 bobRossObj。它实际指向全局对象,也就是说 this.username 不存在。似乎我们没什么办法了。

轮到绑定上场了!

明确绑定 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 实际上是箭头函数。

箭头函数

你也许注意到了箭头函数最近很流行。人们喜欢箭头函数,因为很简洁、很优雅。而且你还知道箭头函数和普通函数有点区别,尽管不太清楚具体区别是什么。

简而言之,两者的区别在于:

在定义箭头函数时,不管 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 按照默认规则指向了全局对象。

你说没有“在对象的上下文中调用”……难道它不是从 initializeFriend 返回的内部调用的吗?如果这还不叫“在对象的上下文中调用”,那我就不知道什么才算了。

我知道“在对象的上下文中调用”这个术语很模糊。也许,判断函数是否“在对象的上下文中调用”的好方法就是检查一遍函数的调用过程,看看是否有个对象“依附”到了函数上。

我们来检查下执行 bobRossObj.onFriendClick() 时的情况。“给我对象 bobRossObj,找到其中的 onFriendClick 然后调用该属性对应的函数”。

我们同样检查下执行 getLastPost() 时的情况。“给我名为 getLastPost 的函数然后执行。”看到了吗?我们根本没有提到对象。

好了,这里有个难题来测试你的理解程度。假设有个函数名为 functionCaller,它的功能就是调用一个函数:

functionCaller(fn) {
  fn()
}

如果调用 functionCaller(bobRossObj.onFriendClick) 会怎样?你会认为 onFriendClick 是“在对象的上下文中调用”的吗?this.username有定义吗?

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

这时可以用箭头函数解决这个问题:

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}`)
    }
  };
}

上述代码的规则是:

在定义箭头函数时,不管 this 指向谁,箭头函数内部的 this 永远指向同一个东西。

箭头函数是在 greeting 中定义的。我们知道,在 greeting 内部的 this 指向对象本身。因此,箭头函数内部的 this 也指向对象本身,这正是我们需要的结果。

结论

this 有时很不好理解,但它对于开发 JavaScript 应用非常有用。本文当然没能介绍 this 的所有方面。一些没有涉及到的话题包括:

  • call 和 apply;

  • 使用 new 时 this 会怎样;

  • 在 ES6 的 class 中 this 会怎样。

我建议你首先问问自己在这些情况下的 this,然后在浏览器中执行代码来检验你的结果。

想学习更多关 于this 的内容,可参考《你不知道的 JS:this 和对象原型》:

  • https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes

如果你想测试自己的知识,可参考《你不知道的JS练习:this和对象原型》:

  • https://ydkjs-exercises.com/this-object-prototypes

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

作者:Austin Tackaberry,Human API 的软件工程师

译者:弯月,责编:屠敏


微信改版了,

想快速看到CSDN的热乎文章,

赶快把CSDN公众号设为星标吧,

打开公众号,点击“设为星标”就可以啦!

征稿啦

CSDN 公众号秉持着「与千万技术人共成长」理念,不仅以「极客头条」、「畅言」栏目在第一时间以技术人的独特视角描述技术人关心的行业焦点事件,更有「技术头条」专栏,深度解读行业内的热门技术与场景应用,让所有的开发者紧跟技术潮流,保持警醒的技术嗅觉,对行业趋势、技术有更为全面的认知。

如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱(guorui@csdn.net)。

推荐阅读:

  • 卖了 43.2 万美元的 AI 画作,其实是借鉴程序员代码的“山寨货”?

  • 亚马逊不仅将弃用 Oracle,还要抢 Java 饭碗!

  • 10 张有关程序员的趣图,图图扎心

  • BCH硬分叉完毕,澳本聪放话:一切尚未结束,游戏继续!

  • Python告诉你:这类程序员最赚钱!

  • 清华夺ASC、ISC、SC三项超算比赛大满贯

  • 刚写完排序算法,就被开除了…

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

  1. 无效内卷正在毁掉年轻一代程序员

    出品 | CSDN(ID:CSDNnews) 内卷本来是一个专业词汇,指一种社会或文化模式发展到一定程度后所表现出的停滞状态,既没有办法趋于稳定,又没有办法升级发展为新的阶段,只能在内部不断进行消耗. ...

  2. java制作扫雷游戏中埋雷的难点_月薪30K程序员花了一个小时,用c++做出经典扫雷游戏 !...

    上次发过一个俄罗斯方块的游戏源码,由于是通过Easy X实现的,但是很多和我一样的新手,一开始不知道Easy X是什么,到时源码拿过去之后,运行报错,我这次发的扫雷, 也是通过Easy X实现,Eas ...

  3. HTML+CSS+JavaScript 制作电子版的烂漫爱心表白动画(程序员也是很烂漫的)

    HTML+CSS+JavaScript 制作电子版的烂漫爱心表白动画(程序员也是很烂漫的) 程序员给大家留下的印象往往是代码的机器,整天写代码-程序员不懂爱!其实这是对程序员一种片面的看法.程序员固然 ...

  4. html+css+javascript实现520告白爱情树(含音乐)程序员表白必备

    ❉ html+css+javascript实现浪漫爱情树 (含音乐)程序员表白必备 一年一度的/520情人节/七夕情人节/生日礼物/告白师妹/圣诞节/元旦节跨年/程序员表白, 引用了CSS3的动画效果 ...

  5. 年薪30W前端程序员,需要吃透的前端书籍推荐

    随着互联网时代的发展,web进入2.0时代,前端开发的岗位逐渐独立出来,大量的前端程序员工资和技术水平飙升.前端框架层出不穷,新技术不断更新,作为前端的程序员也是倍感吃力.但为了高薪,每一个前端开发者 ...

  6. 前端程序员,需要吃透的前端书籍推荐

    随着互联网时代的发展,web进入2.0时代,前端开发的岗位逐渐独立出来,大量的前端程序员工资和技术水平飙升.前端框架层出不穷,新技术不断更新,作为前端的程序员也是倍感吃力.但为了高薪,每一个前端开发者 ...

  7. “非计算机专业如何转行做程序员” - 我的经验

    前两天在微博上看到关于"非计算机专业如何转行做程序员" 的讨论: 讨论中一片学生的来信,勾起我写一篇博客的冲动: 希望我的经验能影响他,影响徘徊在计算机行业外想进来的人. 先做个自 ...

  8. Java程序员从笨鸟到菜鸟之(八十七)跟我学jquery(三)jquery动态创建元素和常用函数示例

    在上面两篇博客中列举了太多的API相信大家看着眼晕. 不过这些基础还必须要讲, 基础要扎实.其实对于这些列表大家可以跳过, 等以后用到时再回头看或者查询官方的API说明.在本博客中就给大家讲解一下这些 ...

  9. 一般项目中哪里体现了数据结构_优秀程序员都应该学习的数据结构与算法项目(GitHub 开源清单)...

    前言 算法为王. 想学好前端,先练好内功,内功不行,就算招式练的再花哨,终究成不了高手:只有内功深厚者,前端之路才会走得更远. 强烈推荐 GitHub 上值得前端学习的数据结构与算法项目,包含 gif ...

最新文章

  1. GridView 实现服务器端和客户端全选的两种方法
  2. LINUX系统管理员技术(Admin)-------第三天
  3. Android 6.0 动态权限申请
  4. 统一项目管理平台(UMPlatForm.NET)-4.7 组织机构管理模块
  5. hashtable遍历
  6. ssas脚本组织程序_微服务架构:从事务脚本到领域模型
  7. Laravel源码分析之Session
  8. (转)找工作是一种必须的生活阅历
  9. php实现一个简单的购物网站
  10. Mac mysql 运行sql文件中文乱码的问题
  11. JAVA读取Properties文件对象常用方法总结
  12. hdfs开机启动流程
  13. grub4dos初级教程-入门篇(Z)
  14. 2015年ps计算机试题,2015年计算机一级考试《PS》模拟试题及答案(一)(2)
  15. 帆软报表参数传给网络报表_报表开发工具FineReport的使用: 程序网络报表
  16. 坦克大战(Tank Battalion)------Java代码实现
  17. NLP算法之一(朴素贝叶斯理论部分)
  18. 从零开始- Android刷机指南一
  19. Qt制作漂亮个性化的界面
  20. 市场因子(Market Factor)——投资组合分析(EAP.portfolio_analysis)

热门文章

  1. 《First Order Motion Model for Image Animation》论文解读
  2. [译]R语言——Shiny框架之入门(三):如何启动一个Shiny应用
  3. leetcode python3 简单题167. Two Sum II - Input array is sorted
  4. 电子换向电动机行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
  5. 一步设置Intellij IDEA 热部署处理方法
  6. java中使用tika_Tika基本使用
  7. mysql case quchong_处理mysql的查询语句去重案例一则
  8. SQL注入学习part03:(结合sqli-libs学习:21-30关)
  9. 文字生成视频,只需一步
  10. Google 宣布 Kotlin-first 已四年,为什么 Java 开发者仍不买账?