【译】理解JavaScript中的This,Bind,Call和Apply

简介: this关键词在JavaScript中是个很重要的概念,也是一个对初学者和学习其他语言的人来说晦涩难懂。在JavaScript中,this是一个对象的引用。this指向的对象可以是基于全局的,在对象上的,或者在构造函数中隐式更改的,当然也可以根据Function原型方法的bind,call和apply使用显示更改的。

image.png


this关键词在JavaScript中是个很重要的概念,也是一个对初学者和学习其他语言的人来说晦涩难懂。在JavaScript中,this是一个对象的引用。this指向的对象可以是基于全局的,在对象上的,或者在构造函数中隐式更改的,当然也可以根据Function原型方法的bindcallapply使用显示更改的。


尽管this是个复杂的话题,但是也是你开始编写第一个JavaScript程序后出现的话题。无论你尝试访问the Document Object Model (DOM)中的元素或事件,还是以面向对象的编程风格来构建用于编写的类,还是使用常规对象的属性和方法,都见遇到this


在这篇文章中,你将学习到基于上下文隐式表示的含义,并将学习如何使用bindcallapply方法来显示确定this的值。


隐式上下文


四个主要上下文中,我们可以隐式地推断出this的值:


  • 全局上下文
  • 作为对象内的方法
  • 作为函数或类的构造函数
  • 作为DOM事件处理程序


全局


在全局上下文中,this指向全局对象。当你使用浏览器,全局上下文将是window。当你使用Node.js,全局上下文就是global


备注:如果你对JavaScript中得作用域概念不熟,你可以去[Understanding Variables, Scope, and Hoisting in JavaScript温习一下。


针对例子,你可以在浏览器的开发者工具栏中验证。如果你不是很熟悉在浏览器中运行JavaScript代码,可以去阅读下How to Use the JavaScript Developer Console 文章。


如果你只是简单打印this,你将看到this指向的对象是什么。


console.log(this)
复制代码


Output
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
复制代码


你可以看到,this就是window,也就是浏览器的全局对象。


Understanding Variables, Scope, and Hoisting in JavaScript中,你学习到函数中的变量有自己的上下文。你可能会认为,在函数内部this会遵循相同的规则,但是并没有。顶层的函数中,this仍然指向全局对象。


你可以写一个顶层的函数,或者是一个没有关联任何对象的函数,比如下面这个:


function printThis() {
  console.log(this)
}
printThis()
复制代码


Output
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
复制代码


即使在一个函数中,this仍然指向了window,或全局对象。


然而,当使用严格模式,全局上下文中,函数内this的上下文指向undefined


'use strict'
function printThis() {
  console.log(this)
}
printThis()
复制代码


Output
undefined
复制代码


总的来说,使用严格模式更加安全,能减少this产生的非预期作用域的可能性。很少有人想直接将this指向window对象。


有关严格模式以及对错误和安全性所做更改的详细信息,请阅读MDN上Strict mode的文档


对象方法


一个方法是对象上的函数,或对象可以执行的一个任务。方法使用this来引用对象的属性。


const america = {
  name: 'The United States of America',
  yearFounded: 1776,
  describe() {
    console.log(`${this.name} was founded in ${this.yearFounded}.`)
  },
}
america.describe()
复制代码


Output
"The United States of America was founded in 1776."
复制代码


在这个例子中,this等同于america


在嵌套对象中,this指向方法当前对象的作用域。在下面这个例子,details对象中的this.symbol指向details.symbol


const america = {
  name: 'The United States of America',
  yearFounded: 1776,
  details: {
    symbol: 'eagle',
    currency: 'USD',
    printDetails() {
      console.log(`The symbol is the ${this.symbol} and the currency is ${this.currency}.`)
    },
  },
}
america.details.printDetails()
复制代码


Output
"The symbol is the eagle and the currency is USD."
复制代码


另一种思考的方式是,在调用方法时,this指向.左侧的对象。


函数构造器


当你使用new关键字,会创建一个构造函数或类的实例。在ECMAScript 2015更新为JavaScript引入类语法之前,构造函数是初始化用户定义对象的标准方法。在Understanding Classes in JavaScript中,你将学到怎么去创建一个函数构造器和等效的类构造函数。


function Country(name, yearFounded) {
  this.name = name
  this.yearFounded = yearFounded
  this.describe = function() {
    console.log(`${this.name} was founded in ${this.yearFounded}.`)
  }
}
const america = new Country('The United States of America', 1776)
america.describe()
复制代码


Output
"The United States of America was founded in 1776."
复制代码


在这个上下文中,现在this绑定到Country的实例,该实例包含在America常量中。


类构造器


类上的构造函数的作用与函数上的构造函数的作用相同。在Understanding Classes in JavaScript中,你可以了解到更多的关于构造函数和ES6类的相似和不同的地方。


class Country {
  constructor(name, yearFounded) {
    this.name = name
    this.yearFounded = yearFounded
  }
  describe() {
    console.log(`${this.name} was founded in ${this.yearFounded}.`)
  }
}
const america = new Country('The United States of America', 1776)
america.describe()
复制代码


describe方法中的this指向Country的实例,即america


Output
"The United States of America was founded in 1776."
复制代码


DOM事件处理程序


在浏览器中,事件处理程序有一个特殊的this上下文。在被称为addEventListener调用的事件处理程序中,this将指向event.currentTarget。开发人员通常会根据需要简单地使用event.targetevent.currentTarget来访问DOM中的元素,但是由于this引用在此上下文中发生了变化,因此了解这一点很重要。


在下面的例子,我们将创建一个按钮,为其添加文字,然后将它追加到DOM中。当我们使用事件处理程序打印其this的值,它将打印目标内容。


const button = document.createElement('button')
button.textContent = 'Click me'
document.body.append(button)
button.addEventListener('click', function(event) {
  console.log(this)
})
复制代码


Output
<button>Click me</button>
复制代码


如果你复制上面的代码到你的浏览器运行,你将看到一个有Click me按钮的页面。如果你点击这个按钮,你会看到<button>Click me</button>出现在控制台上,因为点击按钮打印的元素就是按钮本身。因此,正如你所看到的,this指向的目标元素,就是我们向其中添加了事件监听器的元素。


显式上下文


在所有的先前的例子中,this的值取决于其上下文 -- 在全局的,在对象中,在构造函数或类中,还是在DOM事件处理程序上。然而,使用call, applybind,你可以显示地决定this应该指向哪。


决定什么时候使用call, applybind是一件很困难的事情,因为它将决定你程序的上下文。当你想使用事件来获取嵌套类中的属性时,bind可能有用。比如,你写一个简单的游戏,你可能需要在一个类中分离用户接口和I/O,然后游戏的逻辑和状态是在另一个类中。由于游戏逻辑需要用户输入,比如按键或点击事件,你可能想要bind事件去获取游戏逻辑类中的this的值。


最重要的部分是,要知道怎么决定this对象指向了哪,这样你就可以像之前章节学的隐式操作那样操作,或者通过下面的三种方法显示操作。


Call 和 Apply


callapply非常相似--它们都调用一个带有特定this上下文和可选参数的函数。callapply的唯一区别就是,call需要一个个的传可选参数,而apply只需要传一个数组的可选参数。


在下面这个例子中,我们将创建一个对象,创建一个this引用的函数,但是this没有明确上下文(其实this默认指向了window)。


const book = {
  title: 'Brave New World',
  author: 'Aldous Huxley',
}
function summary() {
  console.log(`${this.title} was written by ${this.author}.`)
}
summary()
复制代码


Output
"undefined was written by undefined"
复制代码


因为summarybook没有关联,调用summary本身将只会打印出undefined,其在全局对象上查找这些属性。


备注: 在严格模式中尝试this会返回Uncaught TypeError: Cannot read property 'title' of undefined的错误结果,因为this它自身将会是undefined


然而,你可以在函数中使用callapply调用book的上下文this


summary.call(book)
// or:
summary.apply(book)
复制代码


Output
"Brave New World was written by Aldous Huxley."
复制代码


现在,当上面的方法运用了,booksummary之间有了关联。我们来确认下,现在this到底是什么。


function printThis() {
  console.log(this)
}
printThis.call(book)
// or:
whatIsThis.apply(book)
复制代码


Output
{title: "Brave New World", author: "Aldous Huxley"}
复制代码


在这个案例中,this实际上变成的所传参数的对象。


这就是说callapply一样,但是它们又有点小区别。


除了将第一个参数作为this上下文传递之外,你也可以传递其他参数。


function longerSummary(genre, year) {
  console.log(
    `${this.title} was written by ${this.author}. It is a ${genre} novel written in ${year}.`
  )
}
复制代码


使用call时,你使用的每个额外的值都会被作为附加参数进行传递。


longerSummary.call(book, 'dystopian', 1932)
复制代码


Output
"Brave New World was written by Aldous Huxley. It is a dystopian novel written in 1932."
复制代码


如果你尝试使用apply去发送相同的参数,就会发生下面的事情:


longerSummary.apply(book, 'dystopian', 1932)
复制代码


Output
Uncaught TypeError: CreateListFromArrayLike called on non-object at <anonymous>:1:15
复制代码


针对apply,作为替代,你需要将参数放在一个数组中传递。


longerSummary.apply(book, ['dystopian', 1932])
复制代码


Output
"Brave New World was written by Aldous Huxley. It is a dystopian novel written in 1932."
复制代码


通过单个参数传递和形成一个数组参数传递,两个之间的差别是微妙的,但是值得你留意。使用apply更加简单和方便,因为如果一些参数的细节改变了,它不需要改变函数调用。


Bind


callapply都是一次性使用的方法 -- 如果你调用带有this上下文的方法,它将含有此上下文,但是原始的函数依旧没改变。


有时候,你可能需要重复地使用方法来调用另一个对象的上下文,所以,在这种场景下你应该使用bind方法来创建一个显示调用this全新函数


const braveNewWorldSummary = summary.bind(book)
braveNewWorldSummary()
复制代码


Output
"Brave New World was written by Aldous Huxley"
复制代码


在这个例子中,每次你调用braveNewWorldSummary,它都会返回绑定它的原始this值。尝试绑定一个新的this上下文将会失败。因此,你始终可以信任绑定的函数来返回你期待的this值。


const braveNewWorldSummary = summary.bind(book)
braveNewWorldSummary() // Brave New World was written by Aldous Huxley.
const book2 = {
  title: '1984',
  author: 'George Orwell',
}
braveNewWorldSummary.bind(book2)
braveNewWorldSummary() // Brave New World was written by Aldous Huxley.
复制代码


虽然这个例子中braveNewWorldSummary尝试再次绑定bind,它依旧保持着第一次绑定就保留的this上下文。


箭头函数


Arrow functions没有自己的this绑定。相反,它们上升到下一个执行环境。


const whoAmI = {
  name: 'Leslie Knope',
  regularFunction: function() {
    console.log(this.name)
  },
  arrowFunction: () => {
    console.log(this.name)
  },
}
whoAmI.regularFunction() // "Leslie Knope"
whoAmI.arrowFunction() // undefined
复制代码


在你想将this执行外部上下文的情况下,箭头函数会很有用。比如,在类中有一个事件监听器,你可能想将this指向此类中的一些值。


在下面这个例子中,像之前一样,你将创建一个按钮并将其追加到DOM中,但是,类中将会有一个事件监听器,当按钮被点击时候会改变其文本值。


const button = document.createElement('button')
button.textContent = 'Click me'
document.body.append(button)
class Display {
  constructor() {
    this.buttonText = 'New text'
    button.addEventListener('click', event => {
      event.target.textContent = this.buttonText
    })
  }
}
new Display()
复制代码


如果你点击按钮,其文本会变成buttonText的值。如果在这里,你并没有使用箭头函数,this将等同于event.currentTarget,如没有显示绑定this,你将不能获取类中的值。这种策略通常使用在像React这样框架的类方法上。


总结


在这篇文章中,你学到了关于JavaScriptthis,和基于隐式运行时绑定的可能具有的不同值,以及通过bindcallapply的显示绑定。你还了解到了如何使用箭头函数缺少this绑定来指向不同的上下文。有了这些知识,你应该能够在你的程序中明确this的价值了。



相关文章
|
20小时前
|
JavaScript 前端开发
深入探索JavaScript:如何改变this的指向
深入探索JavaScript:如何改变this的指向
|
7天前
|
前端开发 JavaScript
【Web 前端】 js中call、apply、bind有什么区别?
【4月更文挑战第22天】【Web 前端】 js中call、apply、bind有什么区别?
【Web 前端】 js中call、apply、bind有什么区别?
|
8天前
|
JavaScript 前端开发
关于javascript中call(),apply(),bind()的介绍,初步
关于javascript中call(),apply(),bind()的介绍,初步
|
10天前
|
JavaScript 前端开发 数据安全/隐私保护
|
12天前
|
自然语言处理 JavaScript 前端开发
js_关键字this指向哪里?
js_关键字this指向哪里?
4 0
|
14天前
|
JavaScript 前端开发
js开发:请解释this关键字在JavaScript中的用法。
【4月更文挑战第23天】JavaScript的this关键字根据执行环境指向不同对象:全局中指向全局对象(如window),普通函数中默认指向全局对象,作为方法调用时指向调用对象;构造函数中指向新实例,箭头函数继承所在上下文的this。可通过call、apply、bind方法显式改变this指向。
8 1
|
1月前
|
JavaScript
call、apply、bind的使用场景区分(js的问题)
call、apply、bind的使用场景区分(js的问题)
|
1月前
|
自然语言处理 JavaScript 前端开发
解析JavaScript中的this:新手常见误区与应对策略
【4月更文挑战第1天】本文介绍了JavaScript中`this`的关键作用及其常见误区,包括作用域与调用上下文混淆、回调函数中的`this`绑定、构造函数使用、对象字面量方法与普通函数的区别以及严格模式的影响。通过理解四条绑定规则(显式、new、隐式、默认)和采取相应避免策略,开发者能更好地掌握和运用`this`,提高编程效率和代码质量。
19 1
|
1月前
|
JavaScript 前端开发 开发者
解密JavaScript中this的指向问题
解密JavaScript中this的指向问题
158 2
|
1月前
|
JavaScript 前端开发
JavaScript中call()与apply()的作用与区别?
JavaScript中call()与apply()的作用与区别?
22 2