Я помню, когда я впервые начал изучать javascript, я не мог понять концепцию ключевого слова this. Я видел общие примеры и не мог понять, в чем смысл их использования. В этой статье я попытаюсь объяснить это так, чтобы было немного понятнее. Путаницу усугубляет то, что в Интернете довольно много статей, в которых одна и та же информация повторяется снова и снова, а некоторые из них даже неверны.

Совет: если вам нравятся подобные статьи в темном режиме с лучшей подсветкой кода, вы можете прочитать эту статью в темном режиме.

В этой статье я попытаюсь объяснить почему, а не случайные фрагменты кода, просто показывая примеры.

В таком примере:

const obj = {
  name: 'john',
  greet() {
    return `hi ${this.name}`
  }
}

Я поймал себя на том, что задаюсь вопросом, почему мы не можем просто сделать это?

const obj = {
  name: 'john',
  greet() {
    return `hi ${obj.name}`
  }
}

Причина немного сложнее, и есть очень веская причина использовать ключевое слово this. Ключевое слово this немного полезнее, и мы не всегда можем понять почему из этих небольших примеров.

Давайте сначала поймем, что this может иметь много разных значений в зависимости от того, как вызывается функция. Значение this связано с тем, как вызывается функция, а не с тем, где функция объявлена.

это ключевое слово не является областью действия!

Область видимости связана с доступностью переменных и определяется во время компиляции.

// name is NOT available here
console.log(name)

function foo() {
  // name is NOT available here

  const name = 'john'

  // name is available here

  function greet() {
    // name is available here
    return `hi ${name}`
  }

  return { greet }
}

Как можно определить это значение?

Выше мы упоминали, что часто люди путают область действия с ключевым словом this. Область действия определяется во время компиляции и относится к доступности переменных, другими словами, откуда к ним можно получить доступ, this с другой стороны, определяется тем, как функция вызывается во время выполнения. Это важное различие, о котором следует помнить. this определяется не тем, где объявлена ​​функция, а тем, как она вызывается.

Давайте рассмотрим некоторые из наиболее распространенных способов определения этого значения. Я попытаюсь объяснить это так, чтобы это имело смысл, потому что во многих онлайн-объяснениях этого не было, когда я пытался узнать, что такое this.

Привязка по умолчанию

Привязка this по умолчанию создается всякий раз, когда функция вызывается в глобальной области. Обратите внимание, что в строгом режиме вы получите undefined.

function foo() {
  console.log(this) // window
}

foo() // here the scope is global

Неявное связывание и методы объекта

Это может немного сбивать с толку, часто неправильно объясняют, что объект владеет методом, а затем добавляют путаницу, говоря, что вы должны использовать только простые функции вместо стрелочных, потому что стрелочные функции не создают свои собственные this. Что такое this внутри объекта? Это должен быть сам объект? Неправильный. this внутри объекта такое же, как this снаружи объекта.

// this === window
const obj = {
  name: 'john',
  // this === window
  greet: function() {
    // this === window
    return `hi ${this.name}`
  },
  greetAgain: () => {
    // this === window
    return `hi ${this.name}`
  }
}

Единственная разница заключается в том, как функция вызывается, как мы уже упоминали. Для вызова метода greet() мы используем запись через точку. Во время вызова значение this определяется тем, что находится слева от точки, что в более технических терминах называется context object.

Здесь, когда вызывается greet(), ему предшествует ссылка на объект контекста с именем obj. Правило неявной привязки говорит об использовании этого объекта в качестве привязки this для вызова функции.

obj.greet() // logs 'hi john'

Приведенный выше код можно переписать следующим образом:

function greet() {
  // this === window
  return `hi ${this.name}`
}

const obj = {
  name: 'john',
  // with es6 shorthand
  greet
}

Теперь мы видим, что в объектном методе нет ничего особенного.

Ниже обратите внимание, что при наличии цепочки для вызова функции имеет значение только последняя ссылка на свойство объекта.

function greet() {
  console.log(`hi ${this.name}`)
}

const obj2 = {
  name: 'david',
  greet
}

const obj = {
  name: 'john',
  obj2
}

obj.obj2.greet() // `hi david`

Явная привязка

Явное связывание происходит при использовании call(), apply(), bind().

Это когда мы явно указываем, каким будет значение this{langague'js'}. call() и apply() в основном одинаковы, отличается только способ передачи им аргументов. call() принимает список, разделенный запятыми, тогда как apply() принимает массив.

function greet() {
  return `hi ${this.name}`
}

const obj = {
  name: 'john',
  greet
}

const obj2 = {
  name: 'david',
  greet
}

obj.greet() // hi john

obj.greet.call(obj2) // hi david

Быстрый обзор

Оба приведенных ниже примера одинаковы:

const obj = {
  name: 'john',
  greet() {
    return `hi ${this.name}`
  }
}

это то же самое, что и:

function greet() {
  return `hi ${this.name}`
}

const obj = {
  name: 'john',
  greet
}

В обоих случаях greet является ссылкой на функцию, которая по умолчанию имеет привязку this по умолчанию.

Неявно потерянный

Это распространенный источник путаницы для разработчиков. Допустим, у вас есть следующий код, и вы хотите сохранить метод объекта, чтобы вы могли вызвать его позже.

const obj = {
  name: 'john',
  greet() {
    return `hi ${this.name}`
  }
}

const greetMethodForLater = obj.greet

greetMethodForLater() // 'hi '
// 'this' is lost

Мы сохранили ссылку на метод obj.greet() в переменной greetMethodForLater, и когда мы вызывали его, это был обычный недекорированный вызов функции в глобальной области видимости. Другими словами, видите ли вы, что используется запись через точку? Есть ли что-нибудь слева от greetMethodForLater()? Ответ отрицательный, это обычный вызов функции, поэтому применяется правило привязки по умолчанию.

Вот где bind() пригодится. Если вы хотите сделать это, чтобы метод работал позже с правильным this, вам нужно использовать bind(). bind возвращает новую функцию, в которой this установлено то, что было передано.

const obj = {
  name: 'john',
  greet() {
    return `hi ${this.name}`
  }
}

const greetMethodForLater = obj.greet.bind(obj)

greetMethodForLater() // 'hi john'

Бинд работает только один раз!

Вы не можете использовать bind два раза подряд.

function greet() {
  console.log(`hi ${this.name}`)
}

const obj1 = {
  name: 'john'
}

const obj2 = {
  name: 'David'
}

const boundFunction = greet.bind(obj1)
boundFunction() // 'hi john'

const attemptToBindFirstFunctionAgain = boundFunction.bind(obj2)
attemptToBindFirstFunctionAgain() // 'hi john'

или более кратко

const boundFunction = greet.bind(obj1).bind(obj2)
boundFunction() // 'hi john'

Обратные вызовы

Обратные вызовы — это еще один способ потерять значение this.

function greet() {
  console.log(`hi ${this.name}`)
}

const obj = {
  name: 'john',
  greet
}

Если бы мы написали:

obj.greet() // 'hi john'

Понятно, что это неявная привязка и ключевое слово this присвоено obj.

Мы рассмотрели выше, почему мы не можем писать:

const greetFunction = obj.greet
greetFunction() // 'hi'

Потому что это обычный, необработанный вызов функции, и поэтому применяется привязка по умолчанию.

То же самое происходит, когда мы используем обратные вызовы.

function callGreet(callbackFn) {
  // callback = obj.greet
  callbackFn()
}

const obj = {
  name: 'john',
  greet() {
    console.log(`hi ${this.name}`)
  }
}

callGreet(obj.greet)

Как callback выглядит внутри функции callGreet?

Давайте посмотрим, как вызывается обратный вызов:

function callGreet(callbackFn) {
  callbackFn = obj.greet
  // another undecorated function invocation with default binding
  callbackFn()
}

Это происходит потому, что callbackFn — это просто ссылка на obj.greet.

Просто для удовольствия давайте возьмем немного более сложный пример:

Как callback выглядит внутри функции forEach?

function forEach(array, callbackFn) {
  for (let i = 0; i < array.length; i++) {
    callbackFn(array[i], i, array)
  }
}

const obj = {
  name: 'john',
  tasks: ['eat', 'clean'],
  listTasks(task) {
    console.log(`${this.name} must ${task}`)
  }
}

forEach(obj.tasks, obj.listTasks)
// this will output
// 'must eat'
// 'must clean

Этот пример немного сложнее, но он позволяет взглянуть на вещи в перспективе и является отличным примером, чтобы понять, почему стрелочные функции могут быть такими полезными.

Теперь внутри функции forEach() давайте посмотрим, как она выглядит во время вызова:

function forEach(array, callbackFn) {
  array = ['eat', 'clean']

  // remember this is just a reference to a function
  // the only time a method (function) belongs to an object is at invocation time
  callbackFn = obj.listTasks
  console.log(callbackFn) 
    // ƒ listTasks(task) {
    //   console.log(`${this.name} must ${task}`)
    // }

  for (let i = 0; i < array.length; i++) {
    // invoked yet again without dot notation, so it's just an undecorated function invocation with default binding
    callbackFn(array[i], i, array)
  }
}

Итак, как мы можем это исправить? Это просто, и следующий пример поможет нам лучше понять стрелочные функции и почему они могут быть такими полезными.

function forEach(array, callbackFn) {
  for (let i = 0; i < array.length; i++) {
    callbackFn(array[i], i, array)
  }
}

const obj = {
  name: 'john',
  tasks: ['eat', 'clean'],
  listTasks(task) {
    console.log(`${this.name} must ${task}`)
  }
}

forEach(obj.tasks, obj.listTasks.bind(obj))

Но что, если мы хотим передать обратный вызов как встроенную функцию, как это обычно бывает?

function forEach(array, callbackFn) {
  for (let i = 0; i < array.length; i++) {
    callbackFn(array[i], i, array)
  }
}

const obj = {
  name: 'john',
  tasks: ['eat', 'clean'],
  listTasks() {
    forEach(
      this.tasks,
      function(task, index, array) {
        console.log(`${this.name} must ${task}`)
      }.bind(this)
    )
  }
}

и в forEach():

function forEach(array, callbackFn) {
  array = ['eat', 'clean']

  // note that this is a bound function
  callbackFn = function(task, index, array) {
    console.log(`${this.name} must ${task}`)
  }
  for (let i = 0; i < array.length; i++) {
    callbackFn(array[i], i, array)
  }
}

В реальном мире использование ключевого слова this в обратном вызове представляет собой проблему. Приведенный ниже код вы часто будете видеть в дикой природе, и я, честно говоря, не советую его использовать, потому что нет причин не использовать синтаксис es6. Синтаксис Pre es6 приводит к тому, что многие блоки кода выглядят следующим образом:

const obj = {
  name: 'john',
  tasks: ['eat', 'clean'],
  listTasks() {
    const _this = this // you may see that insteat of _this as well
    this.tasks.forEach(function(task) {
      console.log(`${_this.name} must ${task}`)
    })
  }
}

Другая вещь, которую вы часто видите, это:

const obj = {
  name: 'john',
  tasks: ['eat', 'clean'],
  listTasks() {
    this.tasks.forEach(function(task) {
      console.log(`${this.name} must ${task}`)
    }.bind(this))
  }
}

Стрелочные функции

Стрелочные функции наследуют свои this от окружающей лексической области видимости, у них нет собственных this.

Если это сбивает с толку, следующий фрагмент кода должен прояснить любую путаницу. Когда я впервые прочитал это в документации, мне всегда было интересно, что это на самом деле означает.

const obj = {
  // this === window
  name: 'john',
  tasks: ['sleep', 'clean'],
  listTasks() {
    // this === window
    this.tasks.forEach(task => {
      // this === window
      console.log(`${this.name} must ${task}`)
    })

    console.log(this) // window
  }
}

// plain reference to listTasks function
const foo = obj.listTasks
foo() // window

Во время вызова определяется this:

const obj = {
  name: 'john',
  tasks: ['sleep', 'clean'],
  listTasks() {
    // 2. Now 'this' is set to 'obj'
    console.log(this) // { name: 'john', tasks: Array(2), listTasks: ƒ }
  }
}

// 1. obj.listTasks is invoked
obj.listTasks()

Итак, когда у нас есть стрелочная функция, она захватывает значение включающей лексической области видимости в во время вызова.

const obj = {
  name: 'john',
  tasks: ['sleep', 'clean'],
  listTasks() {
    // 2. this === obj since the function is currently being invoked
    this.tasks.forEach(task => {
      // 3. 'this' is the inherited value inside of our arrow function
      console.log(`${this.name} must ${this}`)
    })
  }
}

// 1. obj.listTasks() invoked with implicit runtime binding rule
obj.listTasks()

Помните, что у стрелочных функций нет собственного this, this наследуется. Обычное значение function() { /* ... */ } this можно изменить неявно с помощью точек или квадратных скобок или явно с помощью call, apply, bind.

Мы отметили, что стрелочные функции фиксируют это во время вызова, поэтому давайте возьмем еще один пример:

function greet() {
  return (name) => {
    console.log(`hi ${this.name}`)
  }
}

const obj = {
  name: 'john'
}

const obj2 = {
  name: 'david'
}

// at call time we specified obj as the this value
// a function was returned and it inherited obj as it's own this at call time
const foo = greet.call(obj)

const foo = greet.call(obj) сохраняет возвращенную функцию с захваченным this, для которого задано значение obj.

Теперь, когда мы вызываем foo, который на самом деле вызывает функцию внутренней стрелки, которая была возвращена

foo.call(obj2)
// 'hi john'
// NOT 'hi david' as we might expect

Причина этого в том, что функция стрелки внутри функции приветствия лексически захватывает значение this во время вызова. Во время вызова this было привязано к obj, foo (ссылка на возвращаемую стрелочную функцию) также привязано к obj. Лексическая привязка не может быть переопределена, поэтому foo.call(obj2) по-прежнему печатает 'hi john' вместо 'hi david'.

Будь осторожен!

Стрелочные функции наследуют лексический контекст из того места, где они определены, что означает, что этот код НЕ будет работать из-за того, где стрелочная функция была определена, а не из-за того, где она используется.

Поэтому это НЕ будет работать:

const logTask = task => {
  console.log(`${this.name} must ${task}`)
}

const obj = {
  name: 'john',
  tasks: ['clean', 'eat'],
  listTasks() {
    this.tasks.forEach(logTask)
  }
}

но это будет работать:

const obj = {
  name: 'john',
  tasks: ['clean', 'eat'],
  listTasks() {
    this.tasks.forEach(task => {
      console.log(`${this.name} must ${task}`)
    })
  }
}

Получить информацию о функции во время ее выполнения

Переменные существуют только во время выполнения функции, после чего они перестают существовать. Теперь, если это так, вам может быть интересно, как вы можете просматривать значения, есть ли способ приостановить функцию в определенный момент? Ответ: да, вы можете приостановить функцию, установив точку останова в инструментах разработчика в вашем браузере, или вы можете просто использовать оператор debugger.

Это простой пример, но в более сложном примере, если вы хотите узнать больше о том, как и где вызывается функция, а также о ее значении this и другой информации, вставьте debugger в свой код и вызовите функцию в dev. инструменты.

const obj = {
  name: 'john',
  tasks: ['sleep', 'clean'],
  listTasks() {
    debugger
    this.tasks.forEach(task => {
      debugger
      console.log(`${this.name} must ${this}`)
    })
  }
}

obj.listTasks()

Явный или неявный?

Просто для обзора: область определяется во время компиляции, а привязка this определяется во время выполнения. Когда функция вызывается, создается контекст выполнения, и внутри этого контекста выполнения есть свойство привязки this.

Итак, теперь, когда мы знаем, что ключевое слово this может меняться в зависимости от того, где оно вызывается, и теперь, когда мы знаем, как найти сайт вызова, нам нужно понять, что происходит, когда существует несколько правил. Здесь мы будем использовать те же самые объекты, чтобы все было понятно.

const obj = {
  name: 'john',
  greet() {
    return `hi ${this.name}`
  }
}

const obj2 = {
  name: 'david',
  greet() {
    return `hi ${this.name}`
  }
}

// implicit runtime binding
obj.greet() // 'hi john'

// explicit runtime binding
obj.greet.call(obj2) // 'hi david'

Явные победы. Привязка по умолчанию имеет самый низкий приоритет, поэтому до этого момента, когда мы находим место вызова функции и смотрим на код, мы знаем, что по умолчанию самый низкий приоритет, неявная привязка obj.greet() имеет более высокий приоритет, а obj.greet.call(obj2) имеет четный приоритет. более высокий приоритет.

новый оператор

Что произойдет, если мы используем новый оператор с объектом?

function setAge(age) {
  this.age = age
}

const obj = {
  name: 'john'
}

// bar is hard bound to obj
const bar = setAge.bind(obj)
bar(30)
console.log(obj.age) // 30

// here bar is called with the new keyword and baz.age === 40 not 30 as we might expect
const baz = new bar(40)
console.log(baz.age) // 40 

Таким образом, мы можем сделать вывод, что новое ключевое слово имеет наивысший приоритет. Когда вы исследуете сайт вызова, спросите себя, использует ли функция новую привязку? если нет, использует ли функция явную привязку? если нет, вызывается ли функция с неявной привязкой? Если ответ по-прежнему отрицательный, мы знаем, что применяется привязка по умолчанию.

Обзор

Стрелочные функции не имеют собственных this. Они наследуют this окружающего лексического объема.

Значение this создается для обычного function() {} тремя способами:

  1. Неявно — contextObj.foo()
  2. Явно — foo.call(someObj)
  3. С новым оператором — new someConstructorFunction(/* ... */)

new имеет наивысший приоритет при определении значения this. call(), apply() и bind() являются вторыми в очереди Правило неявной привязки является третьим И наконец, если ни одно из вышеперечисленных правил не применяется, то значение этого определяется привязкой по умолчанию правило.

Объектные методы не принадлежат объектам, они являются ссылками на обычные функции. Когда вызывается метод объекта, действует правило неявной привязки, и функция смотрит на то, что находится слева от точки.

Следовательно, оба приведенных ниже примера одинаковы:

Пример 1

const obj = {
  name: 'john',
  greet() {
    return `hi ${this.name}`
  }
}

obj.greet() // 'hi john'

Пример 2

function greet() {
  return `hi ${this.name}`
}

const obj = {
  name: 'john',
  greet
}

Стрелочные функции не имеют собственного значения this, а наследуют его от окружающей лексической области видимости.

const obj = {
  // this === window
  name: 'john',
  greet() {
    // this === window
    return `hi ${this.name}`
  }
}

в во время звонка

const obj = {
  // this === window
  name: 'john',
  greet() {
    // 2. now it's call-time and this === obj
    return `hi ${this.name}`
  }
}

// 1. obj.greet() is invoked with implicit binding rule
obj.greet()
const obj = {
  // this === window
  name: 'john',
  greet: () => {
    // 2. now it's call-time and the surrounding lexical scope is window
    return `hi ${this.name}`
  }
}

// 1. obj.greet() is invoked with implicit binding rule
obj.greet()

Вы можете проверить это, установив свойство объекта и проверив его значение:

const obj = {
  test: this === window,
  name: 'john',
  greet: () => {
    // 2. now it's call-time and the this value is inherited from the
    // surrounding lexial scope. The value of test is the surrounding
    // lexical scope
    return `hi ${this.name}`
  }
}

// 1. obj.greet() is invoked with implicit binding rule
obj.greet()

console.log(obj)
// {test: true, name: 'john', greet: ƒ}

Я надеялся, что это помогло, это руководство, которое я хотел бы иметь, когда впервые начал изучать, как это работает.