简单介绍this用法

1.  this 适合你吗?javascript

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

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

在深刻介绍 this 以前,咱们首先须要理解函数式编程和面向对象编程之间的区别。github

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

你可能不知道,JavaScript 同时拥有面向对象和函数式的结构,因此你能够本身选择用哪一种风格,或者二者都用。数组

很早之前在使用 JavaScript 时就喜欢函数式编程,并且会像躲避瘟疫同样避开面向对象编程,由于不理解面向对象中的关键字,好比 this。浏览器

在某种意义上,也许你能够只专一于一种结构而且彻底忽略另外一种,但这样你只能是一个 JavaScript 开发者。为了解释函数式和面向对象之间的区别,下面咱们经过一个数组来举例说明,数组的内容是 Facebook 的好友列表。闭包

假设你要作一个 Web 应用,当用户使用 Facebook 登陆你的 Web 应用时,须要显示他们的 Facebook 的好友信息。你须要访问 Facebook 并得到用户的好友数据。这些数据多是 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts 等信息。app

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 得到的。如今须要将其转换成方便你的项目使用的格式。咱们假设你想显示的好友信息以下:dom

  • 姓名,格式为`${firstName} ${lastName}`

  • 三篇随机文章

  • 距离生日的天数

 

3. 函数式方式

函数式的方式就是将整个数组或者数组中的某个元素传递给某个函数,而后返回你须要的信息:

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

首先咱们有 Facebook API 返回的原始数据。为了将其转换成须要的格式,首先要将数据传递给一个函数,函数的输出是(或者包含)通过修改的数据,这些数据能够在应用中向用户展现。

咱们能够用相似的方法得到随机三篇文章,而且计算距离好友生日的天数。

函数式的方式是:将原始数据传递给一个函数或者多个函数,得到对你的项目有用的数据格式。

 

4. 面向对象的方式

对于编程初学者和 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

面向对象的方式就是为数据建立对象,每一个对象都有本身的状态,而且包含必要的信息,可以生成须要的数据。

 

5. 这跟 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 出场了!

 

6. 终于——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 不存在。彷佛咱们没什么办法了。

轮到绑定上场了!

 

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

 

8. 箭头函数

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

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

在定义箭头函数时,无论 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 也指向对象自己,这正是咱们须要的结果。

 

9. 结论

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 的软件工程师译者:弯月,责编:屠敏
相关文章
相关标签/搜索