Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
reng99 opened this issue Nov 7, 2019 · 0 comments
Open

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

reng99 opened this issue Nov 7, 2019 · 0 comments
Labels
javascript javascript tag

Comments

@reng99
Copy link
Owner

reng99 commented Nov 7, 2019

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

参考

@reng99 reng99 added the javascript javascript tag label Nov 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javascript javascript tag
Projects
None yet
Development

No branches or pull requests

1 participant