Передать правильный контекст this для обратного вызова setTimeout?

251

Как передать контекст в setTimeout? Я хочу позвонить, this.tip.destroy()если this.options.destroyOnHideпосле 1000 мс. Как я могу это сделать?

if (this.options.destroyOnHide) {
     setTimeout(function() { this.tip.destroy() }, 1000);
} 

Когда я пытаюсь выше, thisотносится к окну.

JamesBrownIsDead
источник
4
Действительно ли дублированный флаг действительно действителен? Этот вопрос на самом деле был задан ранее.
Sui Dream
1
if (this.options.destroyOnHide) {setTimeout (function () {this.tip.destroy ()} .bind (this), 1000); }
Zibri

Ответы:

353

РЕДАКТИРОВАТЬ: Подводя итог, еще в 2010 году, когда этот вопрос был задан, наиболее распространенным способом решения этой проблемы было сохранение ссылки на контекст, в котором setTimeoutвыполняется вызов функции, потому что setTimeoutвыполняет функцию с thisуказанием на глобальный объект:

var that = this;
if (this.options.destroyOnHide) {
     setTimeout(function(){ that.tip.destroy() }, 1000);
} 

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

if (this.options.destroyOnHide) {
     setTimeout(function(){ this.tip.destroy() }.bind(this), 1000);
}

bindФункция создает новую функцию сthis значением предварительно заполненным.

Теперь в современном JS именно эту проблему решают функции стрелок в ES6 :

if (this.options.destroyOnHide) {
     setTimeout(() => { this.tip.destroy() }, 1000);
}

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

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

if (this.options.destroyOnHide) {
     setTimeout(function(that){ that.tip.destroy() }, 1000, this);
}

Смотрите также:

CMS
источник
3
Оно работает. Я протестировал концепцию с помощью jsbin-скрипта: jsbin.com/etise/7/edit
Джон К
1
Этот код включает создание ненужной переменной (которая имеет область действия всей функции); если вы правильно передали thisфункцию, вы бы решили эту проблему для этого случая, для map (), forEach () и т. д., и т. д., используя меньше кода, меньше циклов ЦП и меньше памяти. *** Смотрите: ответ Миши Рейзлина.
HoldOffHunger
222

Есть готовые ярлыки (синтаксический сахар) для функции-обёртки @CMS, с которой ответили. (Ниже предполагается, что контекст, который вы хотите, есть this.tip.)


ECMAScript 5 ( текущие браузеры , Node.js) и Prototype.js

Если вы используете браузер, совместимый с ECMA-262, 5-е издание (ECMAScript 5) или Node.js , вы можете использовать его Function.prototype.bind. При желании вы можете передать любые аргументы функции для создания частичных функций .

fun.bind(thisArg[, arg1[, arg2[, ...]]])

Опять же, в вашем случае попробуйте это:

if (this.options.destroyOnHide) {
    setTimeout(this.tip.destroy.bind(this.tip), 1000);
}

Та же функциональность также была реализована в Prototype (какие-либо другие библиотеки?).

Function.prototype.bindможет быть реализовано следующим образом, если вы хотите обратную совместимость (но, пожалуйста, обратите внимание на примечания).


ECMAScript 2015 ( некоторые браузеры , Node.js 5.0.0+)

Для передовых разработок (2015) вы можете использовать функции жирной стрелки , которые являются частью спецификации ECMAScript 2015 (Harmony / ES6 / ES2015) ( примеры ).

Функции стрелка выражение (также известное как жир функция стрелки ) имеет более короткий синтаксис по сравнению с функциональными выражениями и лексический связывает thisзначение [...].

(param1, param2, ...rest) => { statements }

В вашем случае попробуйте это:

if (this.options.destroyOnHide) {
    setTimeout(() => { this.tip.destroy(); }, 1000);
}

JQuery

Если вы уже используете jQuery 1.4+, есть готовая функция для явного задания thisконтекста функции.

jQuery.proxy () : принимает функцию и возвращает новую, которая всегда будет иметь определенный контекст.

$.proxy(function, context[, additionalArguments])

В вашем случае попробуйте это:

if (this.options.destroyOnHide) {
    setTimeout($.proxy(this.tip.destroy, this.tip), 1000);
}

Underscore.js , lodash

Он доступен в Underscore.js, а также в lodash, как _.bind(...)1 , 2

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

_.bind(function, object, [*arguments])

В вашем случае попробуйте это:

if (this.options.destroyOnHide) {
    setTimeout(_.bind(this.tip.destroy, this.tip), 1000);
}

Джоэл Пурра
источник
Почему не по умолчанию func.bind(context...)? Я что-то пропустил?
aTei
Является ли целесообразным постоянно создавать новую функцию (которую выполняет привязка) каждый раз, когда вы вызываете это? У меня есть тайм-аут поиска, который сбрасывается после каждого нажатия клавиши, и мне кажется, что я должен где-то кэшировать этот «связанный» метод для повторного использования.
Триынко
@Triynko: Я бы не стал связывать функцию дорогостоящей операцией, но если вы вызываете одну и ту же связанную функцию несколько раз, вы также можете сохранить ссылку: var boundFn = fn.bind(this); boundFn(); boundFn();например.
Джоэл Пурра
30

В браузерах, отличных от Internet Explorer, вы можете передавать параметры в функцию вместе после задержки:

var timeoutID = window.setTimeout(func, delay, [param1, param2, ...]);

Итак, вы можете сделать это:

var timeoutID = window.setTimeout(function (self) {
  console.log(self); 
}, 500, this);

Это лучше с точки зрения производительности, чем поиск по области (кэширование thisв переменную вне выражения тайм-аута / интервала), а затем создание замыкания (с помощью $.proxyили Function.prototype.bind).

Код, чтобы заставить его работать в IE от Webreflection :

/*@cc_on
(function (modifierFn) {
  // you have to invoke it as `window`'s property so, `window.setTimeout`
  window.setTimeout = modifierFn(window.setTimeout);
  window.setInterval = modifierFn(window.setInterval);
})(function (originalTimerFn) {
    return function (callback, timeout){
      var args = [].slice.call(arguments, 2);
      return originalTimerFn(function () { 
        callback.apply(this, args) 
      }, timeout);
    }
});
@*/
Миша Рейзлин
источник
1
Когда создается класс с использованием цепочки прототипов, а ваши методы являются методами-прототипами ... «связать» - единственное, что изменит то, что «это» внутри метода. Передавая параметр в обратный вызов, вы не изменяете значение this в функции, поэтому такая функция-прототип не может быть написана с использованием «this» внутри нее, как любой другой метод-прототип. Это приводит к несогласованности. Привязка - это самая близкая вещь к тому, что мы на самом деле хотим, и замыкание может быть кэшировано в «this» для более высокой производительности поиска и не нужно создавать его более одного раза.
Трийнко
4

ПРИМЕЧАНИЕ: это не будет работать в IE

var ob = {
    p: "ob.p"
}

var p = "window.p";

setTimeout(function(){
    console.log(this.p); // will print "window.p"
},1000); 

setTimeout(function(){
    console.log(this.p); // will print "ob.p"
}.bind(ob),1000);
gumkins
источник
2

Если вы используете underscore, вы можете использовать bind.

Например

if (this.options.destroyOnHide) {
     setTimeout(_.bind(this.tip.destroy, this), 1000);
}
Сэм
источник