阅读 1305

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

banner

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的价值了。

参考

喜欢的话就来个赞哦@~@!