Pavel Nakonechnyy

JavaScript: bind против apply и call

Опубликовано by Pavel Nakonechnyy on (изменено: ) в Web development.

Перевод статьи hacksparrow

В чем смысл использования bind, когда у нас уже есть apply и call? Да и что вообще такое bind и где его использовать? Это мы подробно разберем в нашей статье на примере следующего кода.

Для тех, кто не знаком с использованием .bind(), .apply() и .call(), приведу небольшое объяснение.

Все три функции являются методами (функциями, прикрепленными к объекту функции), которые вызывают функцию с кастомным this контекстом.

apply

showDetails.apply() обработает функцию showDetails с fruit в качестве this контекста. Параметры функции будут переданы в массиве следующим аргументом после контекста, как показано в примере ниже.

showDetails.apply(fruit, ['small', 1])
// -> Apple small: $1/kg

call

showDetails.call() вызовет функцию showDetails с fruit в качестве контекста, но параметры будут переданы через запятую после объекта-контекста.

showDetails.call(fruit, 'medium', 5)
// -> Apple medium: $5/kg

bind

bind довольно необычен: он вернет функцию, которая будет вызывать showDetails с fruit в качестве контекста.

var bound = showDetails.bind(fruit, 'large', 10)
bound()
// -> Apple large: $10/kg
var bound = showDetails.bind(fruit)
bound('large', 10)
// -> Apple large: $10/kg

Как вы могли заметить, .apply() и .call() вызываются сразу же, в то время как .bind() возвращает функцию, которая может быть вызвана позже. Но вы можете вызвать ее и сразу же

showDetails.bind(fruit, 'large', 10)()
// -> Apple large: $10/kg

Это возвращает нас к вопросу, зачем нам использовать bind, если мы могли ограничиться call или apply?

Несмотря на то, что bind похож на apply и call своей возможностью определять контекс функции, возможности bind гораздо шире:

  1. bind позволяет задавать аргументы функции при вызове .bind() или при выполнении функции с новым контекстом:
showDetails.bind(fruit, 'large', 10)()
// -> Apple large: $10/kg
var bound = showDetails.bind(fruit)
bound('medium', 7)
// -> Apple medium: $7/kg

Вы можете встретиться с ситуациями, когда у вас есть нужный контекст, но параметры станут известны только позднее (например, при использовании ООП). В таком случае bind — ваш незаменимый помощник.

  1. bind сохраняет контекст this для будущего использования

Вам когда-либо приходилось применять var that = this или var self = this? При использовании bind вам это больше не потребуется.

Без bind:

var cat = {
  name: 'Neo',
  showName: function () {

    var self = this

    // здесь может быть любая функция, меняющая контекст: async/callback
    setTimeout(function () {
      console.log(self.name)
      // `this.name` будет undefined
    })

  }
}

cat.showName()
// -> Neo

С bind:

var cat = {
  name: 'Neo',
  showName: function () {

    var printer = function () {
      console.log(this.name)
    }.bind(this)
    // прикрепляем текущий контекст `this` (ссылка на `cat`)
    // к `printer` функции, пока у нас есть ссылка на контекст
    setTimeout(printer)
  }
}

cat.showName()
// -> Neo

Также контекст this сохраняется с использованием ES6 arrow functions.

  1. bind может сохранять текущие параметры для будущего использования
    Вам когда-нибудь приходилось вызывать анонимную функцию, чтобы перевести параметры в функцию, которая иначе потеряла бы контекст? С bind у вас есть более чистый способ решения таких ситуаций.

Например, к моменту когда код ниже выполняет console.log(pets[i]) i уже равно 3, а потому выведет undefined три раза, вместо ожидаемых имен питомцев.

var pets = ['Neo', 'Bud', 'Kalia']

for (var i = 0; i < pets.length; i++) {
  // симуляция асинхронных операций
  setTimeout(function () {
    console.log(pets[i])
  })
}

Проблема может быть решена созданием замыкания на каждой итерации и передаче текущего значения i ему, так чтобы каждый setTimeout получил своё изолированное значение индекса, не зависящее от финального значения i.

for (var i = 0; i < pets.length; i++) {

  (function (i) {
    setTimeout(function () {
      console.log(pets[i])
    })
  })(i)
}

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

Мы можем переписать код с использованием bind без необходимости создания дополнительного замыкания:

for (var i = 0; i < pets.length; i++) {

  var printer = function (i) {
    console.log(pets[i])
  }.bind(undefined, i)

  setTimeout(printer)
}

Таковы три хорошие причины зачем стоит использовать bind вместо apply или call.

Каждый раз, когда вы работаете с async/вложенным кодом и чувствуете, что вам нужно изощряться чтобы сохранить контекст, вероятно вам стоит использовать bind.

И еще чужое видео на ту же тему, на случай если кто-то не понял моего объяснения:

809