Lewati konteks "ini" yang benar ke callback setTimeout?

249

Bagaimana saya meneruskan konteks setTimeout? Saya ingin menelepon this.tip.destroy()jika this.options.destroyOnHidesetelah 1000 ms. Bagaimana saya bisa melakukan itu?

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

Ketika saya mencoba di atas, thismerujuk ke jendela.

JamesBrownIsDead
sumber
4
Apakah flag duplikat benar-benar valid? Pertanyaan ini sebenarnya ditanyakan sebelumnya.
Sui Dream
1
if (this.options.destroyOnHide) {setTimeout (function () {this.tip.destroy ()} .bind (this), 1000); }
Zibri

Jawaban:

352

EDIT: Singkatnya, pada tahun 2010 ketika pertanyaan ini diajukan, cara paling umum untuk menyelesaikan masalah ini adalah dengan menyimpan referensi ke konteks tempat setTimeoutpemanggilan fungsi dilakukan, karena setTimeoutmenjalankan fungsi dengan thismenunjuk ke objek global:

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

Dalam spec ES5, baru dirilis setahun sebelum waktu itu, memperkenalkan bindmetode ini, ini tidak disarankan dalam jawaban asli karena belum didukung secara luas dan Anda memerlukan polyfill untuk menggunakannya tetapi sekarang ada di mana-mana:

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

The bindFungsi menciptakan fungsi baru dengan thisnilai pra-diisi.

Sekarang di JS modern, inilah persisnya fungsi-fungsi tanda panah menyelesaikan masalah di ES6 :

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

Fungsi panah tidak memiliki thisnilai sendiri, ketika Anda mengaksesnya, Anda mengakses thisnilai lingkup leksikal yang dilampirkan.

HTML5 juga membuat timer standar pada tahun 2011, dan Anda dapat memberikan argumen sekarang ke fungsi callback:

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

Lihat juga:

CMS
sumber
3
Berhasil. Saya menguji konsep dengan skrip jsbin
John K
1
Kode ini melibatkan pembuatan variabel yang tidak perlu (yang memiliki lingkup fungsi-lebar); jika Anda dengan benar masuk thiske fungsi, Anda akan menyelesaikan masalah ini untuk kasus ini, untuk map (), forEach (), dll., menggunakan kode yang lebih sedikit, lebih sedikit siklus CPU, dan lebih sedikit memori. *** Lihat: jawaban Misha Reyzlin.
HoldOffHunger
222

Ada pintasan siap pakai (gula sintaksis) ke fungsi wrapper @CMS dijawab dengan. (Di bawah ini dengan asumsi bahwa konteks yang Anda inginkan adalah this.tip.)


ECMAScript 5 ( browser saat ini , Node.js) dan Prototype.js

Jika Anda menargetkan browser yang kompatibel dengan ECMA-262, edisi ke-5 (ECMAScript 5) atau Node.js , Anda dapat menggunakannya Function.prototype.bind. Anda bisa memberikan argumen fungsi apa pun untuk membuat fungsi parsial .

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

Sekali lagi, dalam kasus Anda, coba ini:

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

Fungsionalitas yang sama juga telah diterapkan di Prototipe (ada perpustakaan lain?).

Function.prototype.binddapat diimplementasikan seperti ini jika Anda ingin kompatibilitas mundur kustom (tapi harap perhatikan catatan).


ECMAScript 2015 ( beberapa browser , Node.js 5.0.0+)

Untuk pengembangan mutakhir (2015), Anda dapat menggunakan fungsi panah gemuk , yang merupakan bagian dari spesifikasi ECMAScript 2015 (Harmony / ES6 / ES2015) ( contoh ).

Sebuah ekspresi panah fungsi (juga dikenal sebagai fungsi panah lemak ) memiliki sintaks yang lebih pendek dibandingkan dengan ekspresi fungsi dan leksikal mengikat thisnilai [...].

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

Dalam kasus Anda, coba ini:

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

jQuery

Jika Anda sudah menggunakan jQuery 1.4+, ada fungsi yang sudah jadi untuk mengatur thiskonteks fungsi secara eksplisit .

jQuery.proxy () : Mengambil fungsi dan mengembalikan yang baru yang akan selalu memiliki konteks tertentu.

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

Dalam kasus Anda, coba ini:

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

Underscore.js , lodash

Ini tersedia di Underscore.js, serta lodash, seperti _.bind(...)1 , 2

bind Mengikat fungsi ke objek, artinya setiap kali fungsi dipanggil, nilaithisakan menjadi objek. Secara opsional, ikat argumen ke fungsi untuk mengisinya terlebih dahulu, juga dikenal sebagai aplikasi parsial.

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

Dalam kasus Anda, coba ini:

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

Joel Purra
sumber
Kenapa tidak default func.bind(context...)? Apakah saya melewatkan sesuatu?
aTei
Apakah performant untuk terus membuat fungsi baru (yang mengikat tidak) setiap kali Anda memanggil ini? Saya memiliki batas waktu pencarian yang ulang setelah setiap penekanan tombol, dan sepertinya saya harus melakukan caching metode 'terikat' ini di suatu tempat untuk digunakan kembali.
Triynko
@ Triynko: Saya tidak akan melihat mengikat fungsi sebagai operasi yang mahal, tetapi jika Anda memanggil fungsi terikat yang sama beberapa kali Anda mungkin juga menyimpan referensi: var boundFn = fn.bind(this); boundFn(); boundFn();misalnya.
Joel Purra
30

Di browser selain Internet Explorer, Anda dapat meneruskan parameter ke fungsi bersama setelah penundaan:

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

Jadi, Anda bisa melakukan ini:

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

Ini lebih baik dalam hal kinerja daripada pencarian lingkup (caching thiske variabel di luar batas waktu / ekspresi interval), dan kemudian membuat penutupan (dengan menggunakan $.proxyatau Function.prototype.bind).

Kode untuk membuatnya berfungsi di IEs dari 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);
    }
});
@*/
Misha Reyzlin
sumber
1
Saat membuat kelas dengan menggunakan rantai prototipe dan metode Anda adalah metode prototipe ... 'bind' adalah satu-satunya hal yang akan mengubah apa 'ini' di dalam metode. Dengan mengirimkan parameter ke callback, Anda tidak mengubah apa 'ini' dalam fungsi, sehingga fungsi prototipe seperti itu tidak dapat ditulis menggunakan 'ini' di dalamnya seperti metode prototipe lainnya. Itu mengarah pada inkonsistensi. Bind adalah hal yang paling dekat dengan apa yang sebenarnya kita inginkan, dan penutupan bisa di-cache dalam 'ini' untuk kinerja pencarian yang lebih tinggi dan tidak harus membuatnya lebih dari sekali.
Triynko
3

CATATAN: Ini tidak akan berfungsi di 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);
permen karet
sumber
2

Jika Anda menggunakan underscore, Anda bisa menggunakan bind.

Misalnya

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