JS: this
Что такое this
This - ключевое слово, которое указывает на контекст исполнения. Контекст исполнения - это объект, в рамках которого функция была вызвана. Но, в отличие от других языков программирования, данный контекст в одной и той же функции в разные моменты времени может отличаться.
This вне функций
Напечатав this
в консоли браузера, мы получим глобальный контекст исполнения – объект window
в браузере.
На самом деле делать так не очень-то и полезно, так как window
или, в последних версиях EcmaScript globalThis
и так всегда доступен из любой части программы.
Для обращения к globalThis
всегда лучше предпочитать явное обращение к нему через window
или globalThis
.
This в произвольной функции / методе
Рассмотрим пример:
function foo() {
console.log(this)
}
foo() // 👈 window
this
в данном случае ссылается на объект, в контексте которого вызвана функция foo
.
А так как никаких других объектов у нас тут нет, то в качестве контекста будет использован window
.
Было очевидно?! Тогда другой пример:
function foo() {
console.log(this)
}
const obj = {
name: 'Hello',
foo: foo, // 👈 используем функцию как метод
}
foo() // 👈 window
obj.foo() // 👈 Object { name: "hello", foo: foo() }
const fooFromObj = obj.foo
fooFromObj() // 👈 window 🤔
Отделив метод от объекта, снова получаем обычную функцию, this
которой указывает на тот объект, в рамках которого она вызывается.
Методы объектов
Любая функция может быть использована в качестве метода объекта. Для этого достаточно передать ее как поле. Все примеры ниже будут работать одинаково:
function getName() {
return this.name
}
const person = {
name: 'Andrew',
// можно передать уже существующую функцию
getName: getName,
// или создать функцию на месте
getName: function getName() {
return this.name
},
// или создать анонимную функцию
// (функцию без имени)
getName: function () {
return this.name
},
// или используем синтаксический сахар
// для более краткой записи.
// В большинстве случаев стоит предпочитать этот способ
getName() {
return this.name
},
}
const name = person.getName()
console.log(name) // 👈 'Andrew'
Привязка контекста
Иногда появляется необходимость указать, какой именно this
у нее должен быть при вызове функции.
Или сделать так, чтобы this
для нее не изменялся от контекста вызова.
Для этого существует 3 метода функции: bind и call, apply.
bind
bind
создает новую функцию, контекст которой не изменяется и всегда является первым аргументом, переданным в bind
.
Пример:
function f() {
console.log(this)
}
f() // 👈 Window
const bound = f.bind(2 /* this = 2 */)
bound() // 👈 2
// Можно сделать предыдущую запись короче:
// Но если нам нужен разовый вызов функции
// с определенным контекстом,
// лучше использовать call или apply
f.bind(2)() // 👈 2
В данном случае this
внутри bound
указывает на число 2.
При этом остальные аргументы bind
будут переданы аргументами в привязанную функцию при ее дальнейшем вызове.
call и apply
Методы call
и apply
используются для вызова функции в определенном контексте.
call
ожидает аргументы функции через запятую, тогда как apply
– массивом.
function f(...numbers) {
const sum = numbers.reduce((a, b) => a + b, 0)
const total = this + sum
console.log(total)
}
f.call(2) // 👈 2, не передали numbers
f.apply(2) // 👈 2, не передали numbers
f.call(2, 3, 4, 5) // 👈 14
f.apply(2, [3, 4, 5]) // 👈 14
// vs
f.bind(2, 3, 4, 5)() // 👈 14
f.bind(2, 3, 4)(5) // 👈 14 🤔 ух ты
Частые примеры использования и проблемы
Использование методов массивов для array-like объектов
В ситуации, когда мы не знаем, сколько аргументов должна принимать функция, можно использовать псевдомассив arguments
, чтобы получить все аргументы вызова (работает только для обычных функций).
Далее, для подсчета суммы можно использовать reduce
, но, так как arguments
– это объект, а не массив, у него такого метода не будет.
Поэтому можно позаимствовать у Array.prototype
этот метод и вызвать его, передавая в качестве контекста arguments
:
function getSum() {
return Array.prototype.reduce.call(
arguments,
(sum, x) => sum + x,
0
)
}
getSum() // 0
getSum(1) // 1
getSum(1, 2) // 3
getSum(1, 2, 3) // 6
Более современное и изящное решение было бы использовать оператор rest
, он же ...
, который преобразует все переданные аргументы в массив:
function getSum(...numbers) {
return numbers.reduce((sum, x) => sum + x, 0)
}
getSum() // 0
getSum(1) // 1
getSum(1, 2) // 3
getSum(1, 2, 3) // 6
Для тех, кому интересно, как работает оператор rest
Код ниже - преобразования babel
, который переводит более современную версию JS в ES2015, для корректной работы всех браузеров, которые могут не поддерживать ES6.
function f(...args) {
console.log(args)
}
Преобразуется в:
function f() {
for (
var _len = arguments.length,
args = new Array(_len),
_key = 0;
_key < _len;
_key++
) {
args[_key] = arguments[_key]
}
console.log(args)
}
Передача метода объекта в HOF (функцию высшего порядка)
const eventHandlers = {
greeting: 'HEY',
onClick(e) {
console.log(this, this.greeting, e.clientX, e.clientY)
},
}
document.addEventListener('click', eventHandlers.onClick)
// Тыкая на экран, можно получить такой вывод:
// HTMLDocument undefined 915 190
Из-за того, что onClick
вызывается где-то в другом контексте, this
безнадежно потерян.
Поэтому, можем воспользоваться bind
, чтобы убедиться, что контекст остается таким, как нам надо:
document.addEventListener(
'click',
eventHandlers.onClick.bind(eventHandlers)
)
// Тыкая на экран, можно получить такой вывод:
// Object { greeting: "HEY", onClick: onClick(e) } HEY 1064 243
Сохраняем контекст навсегда
В предыдущем примере можно сделать еще более безопасный механизм, который предотвратит потерю контекста. Для этого можно сделать так:
const eventHandlers = {
greeting: 'HEY',
onClick(e) {
console.log(this, this.greeting, e.clientX, e.clientY)
},
}
// Привязываем веревками
eventHandlers.onClick =
eventHandlers.onClick.bind(eventHandlers)
// теперь всегда передаем уже привязанную функцию
document.addEventListener('click', eventHandlers.onClick)
document
.querySelector('.button')
.addEventListener('click', eventHandlers.onClick)
// Тыкая на экран, можно получить такой вывод:
// Object { greeting: "HEY", onClick: onClick(e) } HEY 1064 243
Важно помнить, что на момент создания объекта, this
все еще указывает на внешний контекст, а не на сам объект, поэтому такой код будет работать не так, как мы могли бы ожидать:
const obj = {
f: function () {
console.log(this) // этот this будет определен в момент вызова
}.bind(this), // а этот `this` уже есть. И в данном случае он window
}
obj.f() // 👈 Window
Код выше аналогичен следующему:
const f = function () {
console.log(this)
}.bind(this)
const obj = {
f: f,
}
obj.f() // 👈 Window
this для реализации вызова цепочкой (chaining)
Что-то схожее реализовано в библиотеке lodash
.
Иногда такой подход позволяет скрыть мутацию данных внутри какого-то класса или объекта и упростить понимание кода.
Например, можно "скрыть" реализацию создания CSV:
const csv = createCSV()
.setHeaders(['id', 'name', 'age'])
.addRow([1, 'Ivan', 18])
.addRow([2, 'Andrew', 25])
.addRow([3, 'Oleg', 20])
function createCSV(header = [], rows = []) {
return {
setHeaders(newHeader) {
header = newHeader
return this
},
setRows(newRows) {
rows = newRows
return this
},
addRow(row) {
rows.push(row)
return this
},
deleteRowAtIndex(i = 0) {
rows.splice(i, 1)
return this
},
toString() {
const data = [
header.join(','),
...rows.map((row) => row.join(',')),
]
return data.join('\n') // 👈 возвращаем значение, а не this. Прерываем "цепочку"
},
get() {
return { header, rows } // 👈 возвращаем значение, а не this. Прерываем "цепочку"
},
}
}
Теперь, когда csv
готов, нам остается только записать его в файл или распечатать в консоли.
const rawData = csv.get()
console.log(rawData)
// 👆
// {
// header: ['id', 'name', 'age'],
// rows: [
// [1, 'Ivan', 18],
// [2, 'Andrew', 25],
// [3, 'Oleg', 20],
// ],
// }
const csvString = csv.toString()
console.log(csvString)
// 👆
// "id,name,age
// 1,Ivan,18
// 2,Andrew,25
// 3,Oleg,20"
this внутри методов массивов (и любых других callback'ов)
const utils = {
hasEvenNumber(...numbers) {
console.log('outside', this)
const hasEven = numbers.some(function (x) {
console.log('inside', this)
return x % 2 === 0
})
},
}
const hasEven = utils.hasEvenNumber([1, 2, 3])
// outside Object { hasEvenNumber: hasEvenNumber(numbers) }
// inside Window 👹
Есть несколько путей решения проблемы.
Самый простой и олдскульный – сохранить this
в переменную:
const utils = {
hasEvenNumber(...numbers) {
const that = this
console.log('outside', this)
const hasEven = numbers.some(function (x) {
console.log('inside', that)
return x % 2 === 0
})
},
}
const hasEven = utils.hasEvenNumber([1, 2, 3])
// outside Object { hasEvenNumber: hasEvenNumber(numbers) }
// inside Object { hasEvenNumber: hasEvenNumber(numbers) }
Более красивый и лаконичный – использовать стрелочную функцию:
const utils = {
hasEvenNumber(...numbers) {
console.log('outside', this)
const hasEven = numbers.some((x) => {
console.log('inside', this)
return x % 2 === 0
})
},
}
const hasEven = utils.hasEvenNumber([1, 2, 3])
// outside Object { hasEvenNumber: hasEvenNumber(numbers) }
// inside Object { hasEvenNumber: hasEvenNumber(numbers) }
И вот мы подошли к необходимости изучить
Стрелочные функции
Стрелочные функции – еще один способ написания функций. Обычно код получается более лаконичным. Зачастую, они работают интуитивно понятнее и проще, однако имеют ряд отличий от обычных функций:
- Не имеют своего
this
.this
всегда указывает на то место в коде, в котором стрелочная функция была создана (а не вызвана, как у обычных функций) - Не имеют псевдомассива
arguments
- Не могут быть функциями-конструкторами (нельзя использовать с ключевым словом
new
)
Самое интересное - это их работа с this
.
Стоит относиться к стрелочной функции так: это обычная функция, которая привязана к this
.
То есть:
const f = () => {
console.log(this)
}
// то же, что и
const f = function () {
console.log(this)
}.bind(this)
Зная это, становится очевидным, что
const obj = {
f: () => {
console.log(this) // всегда Window
},
}
При этом, внутри классов у стрелочных функций this
будет всегда указывать на экземпляр:
class Person {
constructor(name) {
this.name = name
}
sayName = () => {
console.log(this.name)
}
}
const ivan = new Person('Ivan')
ivan.sayName() // 👈 Ivan
const say = ivan.sayName
say() // 👈 Ivan, потому что функция привязана!
Вывод
this
в JS – довольно странная и сложная тема.
Работа с контекстом в JS отличается от работы this
в других языках программирования.
Стрелочные функции помогают сделать нашу жизнь проще в большинстве случаев.
Однако не всегда они являются лучшим решением проблемы.
Есть ситуации, в которых стоит предпочитать обычные функции.