深度解析JavaScript的this关键字
2021-03-09 20:28
函数式方法
如果使用函数式方法,就是将整个数组或数组的每个元素传给一个返回所需操作数据的函数:
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:
然后是 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
【限时优惠】
极客时间《人工智能基础课》专栏里,工学博士、副教授王天一会带你巩固人工智能基础,梳理人工智能知识框架,了解人工智能的最佳应用场景。
现在订阅《人工智能基础课》专栏,限时拼团价¥49,11 月 5 日立即恢复原价¥68。
订阅后,每邀请一位好友购买,你可获得 ¥12 现金返现,好友也将获得 ¥6 返现。多邀多得,上不封顶,立即提现。
扫码或点击「阅读原文」,立享优惠。
文章标题:深度解析JavaScript的this关键字
文章链接:http://soscw.com/index.php/essay/62437.html