Я помню, когда я впервые начал изучать 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() {}
тремя способами:
- Неявно —
contextObj.foo()
- Явно —
foo.call(someObj)
- С новым оператором —
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: ƒ}
Я надеялся, что это помогло, это руководство, которое я хотел бы иметь, когда впервые начал изучать, как это работает.