Как получить доступ к правильному `this` внутри обратного вызова?

У меня есть функция-конструктор, которая регистрирует обработчик событий:

function MyConstructor(data, transport) {
    this.data = data;
    transport.on('data', function () {
        alert(this.data);
    });
}

// Mock transport object
var transport = {
    on: function(event, callback) {
        setTimeout(callback, 1000);
    }
};

// called as
var obj = new MyConstructor('foo', transport);

Ответы

Ответ 1

Что вы должны знать об this

this (иначе говоря, "контекст") является специальным ключевым словом внутри каждой функции, и его значение зависит только от того, как была вызвана функция, а не от того, как/когда/где она была определена. На него не влияют лексические области, такие как другие переменные (кроме функций стрелок, см. Ниже). Вот некоторые примеры:

function foo() {
    console.log(this);
}

// normal function call
foo(); // 'this' will refer to 'window'

// as object method
var obj = {bar: foo};
obj.bar(); // 'this' will refer to 'obj'

// as constructor function
new foo(); // 'this' will refer to an object that inherits from 'foo.prototype'

Чтобы узнать больше об this, ознакомьтесь с документацией MDN.


Как обратиться к правильному this

Не используйте this

Вы фактически не хотите получать доступ к this в частности, но к объекту, на который он ссылается. Поэтому простое решение состоит в том, чтобы просто создать новую переменную, которая также относится к этому объекту. Переменная может иметь любое имя, но общие - это self и that.

function MyConstructor(data, transport) {
    this.data = data;
    var self = this;
    transport.on('data', function() {
        alert(self.data);
    });
}

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

Явным образом установите this для обратного вызова - часть 1

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

Каждая функция имеет метод .bind [docs], который возвращает новую функцию с this привязкой к значению. Функция имеет то же поведение, что и тот, который вы назвали .bind on, только то, что this было задано вами. Независимо от того, как и когда эта функция вызывается, this всегда относится к переданному значению.

function MyConstructor(data, transport) {
    this.data = data;
    var boundFunction = (function() { // parenthesis are not necessary
        alert(this.data);             // but might improve readability
    }).bind(this); // <- here we are calling '.bind()' 
    transport.on('data', boundFunction);
}

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

Примечание. При связывании контекста для jQuery вместо этого используйте jQuery.proxy [docs]. Причина этого заключается в том, что вам не нужно сохранять ссылку на функцию при отмене обратного вызова события. jQuery обрабатывает это внутренне.

ECMAScript 6: использование функций стрелок

В ECMAScript 6 представлены функции стрелок, которые можно рассматривать как лямбда-функции. Они не имеют свои собственные this связывание. Вместо this это рассматривается в области видимости как обычная переменная. Это означает, что вам не нужно вызывать .bind. Это не единственное особое поведение, которое у них есть, для получения дополнительной информации обратитесь к документации MDN.

function MyConstructor(data, transport) {
    this.data = data;
    transport.on('data', () => alert(this.data));
}

Установите this для обратного вызова - часть 2

Некоторые функции/методы, которые принимают обратные вызовы также принимают значение, к которому обратному вызову this следует ссылаться. Это в основном то же самое, что и привязывать его самостоятельно, но функция/метод делает это за вас. Array#map [docs] - такой метод. Его подпись:

array.map(callback[, thisArg])

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

var arr = [1, 2, 3];
var obj = {multiplier: 42};

var new_arr = arr.map(function(v) {
    return v * this.multiplier;
}, obj); // <- here we are passing 'obj' as second argument

Примечание. Независимо от того, можете ли вы передать значение для this, обычно упоминается в документации этой функции/метода. Например, метод jQuery $.ajax [docs] описывает параметр, называемый context:

Этот объект станет контекстом всех обратных вызовов, связанных с Ajax.


Общая проблема: использование объектных методов в качестве обработчиков обратных вызовов/событий

Другим распространенным проявлением этой проблемы является то, что объектный метод используется как обработчик обратного вызова/события. Функции являются первоклассными гражданами в JavaScript, а термин "метод" - просто разговорный термин для функции, которая является значением свойства объекта. Но эта функция не имеет конкретной ссылки на ее "содержащий" объект.

Рассмотрим следующий пример:

function Foo() {
    this.data = 42,
    document.body.onclick = this.method;
}

Foo.prototype.method = function() {
    console.log(this.data);
};

Функция this.method назначается как обработчик события клика, но если щелкнуть document.body, зарегистрированное значение будет undefined, потому что внутри обработчика события this относится к document.body, а не к экземпляру Foo.
Как уже упоминалось в начале, this относится к тому, как называется функция, а не как она определена.
Если код выглядит следующим образом, может быть более очевидным, что функция не имеет неявной ссылки на объект:

function method() {
    console.log(this.data);
}


function Foo() {
    this.data = 42,
    document.body.onclick = this.method;
}

Foo.prototype.method = method;

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

document.body.onclick = this.method.bind(this);

или явно вызвать функцию как "метод" объекта, используя анонимную функцию в качестве обработчика обратного вызова/события и назначить объект (this) другой переменной:

var self = this;
document.body.onclick = function() {
    self.method();
};

или используйте функцию стрелки:

document.body.onclick = () => this.method();

Ответ 2

Вот несколько способов доступа к родительскому контексту внутри дочернего контекста -

  1. Вы можете использовать функцию bind().
  2. Храните ссылку на контекст/внутри внутри другой переменной (см. Пример ниже).
  3. Используйте функции ES6 Arrow.
  4. Alter Code/function design/architecture - для этого вы должны иметь команду над шаблонами проектирования в javascript.

1. Используйте функцию bind()

function MyConstructor(data, transport) {
    this.data = data;
    transport.on('data', ( function () {
        alert(this.data);
    }).bind(this) );
}
// Mock transport object
var transport = {
    on: function(event, callback) {
        setTimeout(callback, 1000);
    }
};
// called as
var obj = new MyConstructor('foo', transport);

Если вы используете underscore.js - http://underscorejs.org/#bind

transport.on('data', _.bind(function () {
    alert(this.data);
}, this));

2 Сохраните ссылку на контекст/внутри внутри другой переменной

function MyConstructor(data, transport) {
  var self = this;
  this.data = data;
  transport.on('data', function() {
    alert(self.data);
  });
}

3 Функция стрелки

function MyConstructor(data, transport) {
  this.data = data;
  transport.on('data', () => {
    alert(this.data);
  });
}

Ответ 3

Все это в "волшебном" синтаксисе вызова метода:

object.property();

Когда вы получаете свойство из объекта и вызываете его за один раз, объект будет контекстом для метода. Если вы вызываете тот же метод, но в отдельных шагах, контекст представляет собой глобальную область (окно):

var f = object.property;
f();

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

this.saveNextLevelData(this.setAll);

То, где вы привязываете контекст к функции:

this.saveNextLevelData(this.setAll.bind(this));

Если вы используете jQuery, вы должны использовать метод $.proxy, а bind не поддерживается во всех браузерах:

this.saveNextLevelData($.proxy(this.setAll, this));

Ответ 4

Беда с "контекстом"

Термин "контекст" иногда используется для обозначения объекта, на который ссылается это. Его использование неуместно, потому что это не соответствует ни семантически, ни технически с ECMAScript this.

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

Это показано в разделе ECMA-262 10.4.2:

Установите для ThisBinding то же значение, что и для ThisBinding вызывающего контекста выполнения.

который ясно указывает на то, что это является частью контекста исполнения.

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

Таким образом, значение этого не "контекст", это просто одна часть контекста исполнения. По сути, это локальная переменная, которая может быть установлена при вызове любого объекта и в строгом режиме для любого значения вообще.

Ответ 5

Во-первых, вам нужно иметь четкое представление о scope и поведении this ключевого слова в контексте области scope.

this & scope :


there are two types of scope in javascript. They are :

   1) Global Scope

   2) Function Scope

Короче говоря, глобальная область действия относится к объекту window.Variables, объявленные в глобальной области, доступны из любого места. С другой стороны, область функций находится внутри функции. переменная, объявленная внутри функции, не может быть доступна из внешнего мира. this ключевое слово в глобальной области видимости относится к объекту окна. this внутренняя функция также относится к объекту window.So, this всегда будет ссылаться на окно, пока мы не найдем способ манипулировать this чтобы указать контекст по нашему собственному выбору.

--------------------------------------------------------------------------------
-                                                                              -
-   Global Scope                                                               -
-   ( globally "this" refers to window object)                                 -     
-                                                                              -
-         function outer_function(callback){                                   -
-                                                                              -
-               // outer function scope                                        -
-               // inside outer function"this" keyword refers to window object -                                                                              -
-              callback() // "this" inside callback also refers window object  -

-         }                                                                    -
-                                                                              -
-         function callback_function(){                                        -
-                                                                              -
-                //  function to be passed as callback                         -
-                                                                              -
-                // here "THIS" refers to window object also                   -
-                                                                              -
-         }                                                                    -
-                                                                              -
-         outer_function(callback_function)                                    -
-         // invoke with callback                                              -
--------------------------------------------------------------------------------

Различные способы управления this внутренней функцией обратного вызова:

Здесь у меня есть функция-конструктор, называемая Person. Он имеет свойство, называемое name и четырьмя методами, называемыми sayNameVersion1, sayNameVersion2, sayNameVersion3, sayNameVersion4. Все четыре из них имеют одну конкретную задачу. Заберите обратный вызов и вызовите его. Обратный вызов имеет конкретную задачу, которая заключается в том, чтобы регистрировать свойство name экземпляра функции конструктора Person.

function Person(name){

    this.name = name

    this.sayNameVersion1 = function(callback){
        callback.bind(this)()
    }
    this.sayNameVersion2 = function(callback){
        callback()
    }

    this.sayNameVersion3 = function(callback){
        callback.call(this)
    }

    this.sayNameVersion4 = function(callback){
        callback.apply(this)
    }

}

function niceCallback(){

    // function to be used as callback

    var parentObject = this

    console.log(parentObject)

}

Теперь позвольте создать экземпляр из конструктора person и вызвать различные версии sayNameVersionX (X относится к 1,2,3,4) методу с niceCallback чтобы узнать, как много способов управлять this внутренним обратным вызовом для обращения к экземпляру person.

var p1 = new Person('zami') // create an instance of Person constructor

bind:

Что нужно сделать, так это создать новую функцию с this ключевым словом с предоставленным значением.

sayNameVersion1 и sayNameVersion2 используют bind, чтобы манипулировать this функцией обратного вызова.

this.sayNameVersion1 = function(callback){
    callback.bind(this)()
}
this.sayNameVersion2 = function(callback){
    callback()
}

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

p1.sayNameVersion1(niceCallback) // pass simply the callback and bind happens inside the sayNameVersion1 method

p1.sayNameVersion2(niceCallback.bind(p1)) // uses bind before passing callback

вызов :

first argument из call метода используется как this внутри функции, которая вызывается с call, прикрепленной к нему.

sayNameVersion3 использует call чтобы манипулировать this чтобы ссылаться на созданный нами объект person, а не на объект окна.

this.sayNameVersion3 = function(callback){
    callback.call(this)
}

и он называется следующим:

p1.sayNameVersion3(niceCallback)

подать выражение :

Подобно call, первый аргумент apply относится к объекту, который будет указан this ключевым словом.

sayNameVersion4 использует apply для управления this обратиться к человеку объекту

this.sayNameVersion4 = function(callback){
    callback.apply(this)
}

и он называется следующим. Просто обратный вызов передается,

p1.sayNameVersion4(niceCallback)

Ответ 6

Мы не можем привязать это к setTimeout(), поскольку он всегда выполняется с глобальным объектом (Window), если вы хотите получить доступ к this контексту в функции обратного вызова, а затем используя функцию bind() для функции обратного вызова, которую мы можем достичь как:

setTimeout(function(){
    this.methodName();
}.bind(this), 2000);

Ответ 7

Вы должны знать об этом ключевом слове.

На мой взгляд, вы можете реализовать "это" тремя способами (Self/Arrow function/Bind Method)

Функция это ключевое слово ведет себя немного иначе в JavaScript по сравнению с другими языками.

Он также имеет некоторые различия между строгим режимом и нестрогим режимом.

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

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

ES5 представил метод bind() для установки значения функции this независимо от того, как она вызывается,

и ES2015 представили функции стрелок, которые не обеспечивают собственную привязку this (она сохраняет значение this лексического контекста).

Метод 1: Self - Self используется для сохранения ссылки на оригинал, даже если контекст меняется. Эта техника часто используется в обработчиках событий (особенно в замыканиях).

Ссылка: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

function MyConstructor(data, transport) {
    this.data = data;
    var self = this;
    transport.on('data', function () {
        alert(self.data);
    });
}

Метод 2: функция стрелки - выражение функции стрелки является синтаксически компактной альтернативой регулярному выражению функции,

хотя и без привязок к ключевым словам this, arguments, super или new.target.

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

Ссылка: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

  function MyConstructor(data, transport) {
    this.data = data;
    transport.on('data',()=> {
        alert(this.data);
    });
}

Метод 3: Bind- Метод bind() создает новую функцию, которая,

при вызове имеет ключевое слово this установленное значение,

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

Ссылка: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind

  function MyConstructor(data, transport) {
    this.data = data;
    transport.on('data',(function() {
        alert(this.data);
    }).bind(this);

Ответ 8

В настоящее время существует другой подход, если классы используются в коде.

С поддержкой полей классов можно сделать следующий путь:

class someView {
    onSomeInputKeyUp = (event) => {
        console.log(this); // this refers to correct value
    // ....
    someInitMethod() {
        //...
        someInput.addEventListener('input', this.onSomeInputKeyUp)

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

Поскольку это предложение Stage 3 вам понадобится babel и соответствующий плагин babel для его обработки как сейчас (08/2018).

Ответ 9

Другой подход, который является стандартным способом, поскольку DOM2 связывает this внутри прослушивателя событий, что позволяет всегда удалять прослушиватель (среди других преимуществ), является методом handleEvent(evt) из интерфейса EventListener:

var obj = {
  handleEvent(e) {
    // always true
    console.log(this === obj);
  }
};

document.body.addEventListener('click', obj);

Подробную информацию об использовании handleEvent можно найти здесь: https://medium.com/@WebReflection/dom-handleevent-a-cross-platform-standard-since-year-2000-5bf17287fd38

Ответ 10

Вопрос вращается вокруг того, как this ключевое слово ведет себя в JavaScript. this ведет себя иначе, как показано ниже,

  1. Значение this обычно определяется контекстом выполнения функций.
  2. В глобальной области this относится к глобальному объекту (объекту window).
  3. Если строгий режим включен для любой функции, то значение this будет undefined как в строгом режиме, глобальный объект ссылается на undefined вместо объекта window.
  4. Объект, который стоит перед точкой, является тем, к чему будет привязано ключевое слово this.
  5. Мы можем установить значение этого явно с помощью call(), bind() и apply()
  6. Когда используется new ключевое слово (конструктор), оно привязывается к создаваемому новому объекту.
  7. Функции стрелки не связывают this - вместо this это связано лексически (то есть основано на оригинальном контексте)

Как подсказывает большинство ответов, мы можем использовать функцию Arrow или метод bind() или Self var. Я бы процитировал пункт о лямбдах (функция стрелки) из руководства по стилю Google JavaScript

Предпочитайте использовать функции стрелок над f.bind (это), и особенно над goog.bind(f, это). Избегайте писать const self = this. Функции стрелок особенно полезны для обратных вызовов, которые иногда передают неожиданные дополнительные аргументы.

Google явно рекомендует использовать лямбды, а не связывать или const self = this

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

function MyConstructor(data, transport) {
  this.data = data;
  transport.on('data', () => {
    alert(this.data);
  });
}

Рекомендации:

  1. https://medium.com/tech-tajawal/javascript-this-4-rules-7354abdb274c
  2. стрелка-функции-против-безвыходном