Belajar dengan membaca secara berurutan

Binding this — this Ditentukan oleh Cara Pemanggilan dan Bisa Dikunci

Method yang dimasukkan ke variabel atau dioper sebagai callback bisa kehilangan this. Membahas TypeError-nya, bind, arrow function field, dan setTimeout.

Kadang kamu mengoper method yang menghitung total keranjang ke kode lain, lalu kode itu memanggilnya belakangan. Method yang berjalan normal sebagai cart.total() bisa berhenti dengan error begitu dimasukkan ke variabel atau dioper ke setTimeout, karena this-nya berubah.

Artikel ini membahas binding this, yang ditentukan oleh cara sebuah function dipanggil, serta cara mengunci this dengan bind atau arrow function.

Error Saat Diambil dari Object — this di Dalam Method

Misalnya kamu ingin memakai cart.total() juga untuk kolom total di halaman, lalu memasukkannya ke variabel dengan const calcTotal = cart.total;. Saat calcTotal() dipanggil, kodenya berhenti sebelum sempat menghitung apa pun, padahal isi keranjangnya sama.

Value yang masuk ke this di dalam method ditentukan oleh baris yang memanggilnya, bukan oleh tempat method itu ditulis. Inilah yang disebut binding this (this diikatkan ke sebuah value). Kalau kamu membaca cart.total tanpa (), yang kamu dapat hanya function-nya; cart tidak ikut terbawa.

Pada calcTotal(), yang tidak punya apa pun di kiri titik, this di dalam class bernilai undefined.

class Cart {
  constructor(items) {
    this.items = items;   // Subtotal tiap produk
  }
  total() {
    return this.items.reduce((sum, price) => sum + price, 0);
  }
}
const cart = new Cart([1980, 3000]);

// Panggil dengan cart di kiri titik
console.log(cart.total());       // 4980

// Masukkan ke variabel / ambil dengan destructuring
const calcTotal = cart.total;
const { total } = cart;
console.log(typeof calcTotal);   // function
// calcTotal();                  // TypeError: Cannot read properties of undefined (reading 'items')
// total();                      // TypeError (alasannya sama)
total yang Sama, this yang Berbeda
Body total()membaca this.itemscart.total()(kiri titik: cart)calcTotal()(dari variabel)total()(destructuring)this berisicartthis berisiundefinedthis berisiundefinedMengembalikan 4980Berhenti denganTypeErrorBerhenti denganTypeError
Ketiganya menjalankan body total yang sama. this berisi cart hanya pada pemanggilan yang menaruh cart di kiri titik.

Kolom tengah dan kanan sama-sama menampilkan TypeError: Cannot read properties of undefined (reading 'items'). Destructuring const { total } = cart; juga hanya memasukkan satu function ke variabel, jadi total() berhenti karena alasan yang sama dengan calcTotal().

Di meja layanan laundry, kamu memakai ulang method yang membuat satu baris status pengerjaan. LaundryItem, shirt, dan coat sudah dideklarasikan.

① Panggil method status milik shirt sebagai method, lalu tampilkan hasilnya.

② Masukkan method yang sama dari shirt ke property line milik coat, lalu panggil dari coat dan tampilkan hasilnya.

③ Ambil method yang sama dari shirt dengan destructuring, lalu panggil langsung.

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Menjaga this Walau Dimasukkan ke Variabel — Method bind

Sekarang kamu ingin mengoper perhitungan total sebagai function ke kode yang menampilkan kolom total. Seperti di bagian sebelumnya, cart.total yang hanya dimasukkan ke variabel tidak membawa cart, jadi hasilnya TypeError begitu kode penerimanya memanggil function itu.

bind (method yang dimiliki setiap function, yang mengembalikan function baru dengan this dikunci ke value yang kamu berikan) ditulis cart.total.bind(cart). Function yang dikembalikan tetap memakai cart sebagai this, bahkan kalau dipanggil tanpa apa pun di kiri titik.

Yang terkunci hanya function yang dikembalikan, jadi pakailah return value itu saat memanggil atau mengopernya.

class Cart {
  constructor(items) { this.items = items; }   // Subtotal tiap produk
  total() { return this.items.reduce((sum, price) => sum + price, 0); }
}
const cart = new Cart([1980, 3000]);

// Function yang hanya diambil dari cart mendapat this undefined saat dipanggil
const unbound = cart.total;
// unbound();                             // TypeError

// bind mengembalikan function baru dengan this ditetapkan ke cart
const boundTotal = cart.total.bind(cart);
console.log(boundTotal());                // 4980
console.log(boundTotal === cart.total);   // false

// Yang terkunci adalah cart itu sendiri, jadi subtotal yang ditambahkan belakangan ikut terbaca
cart.items.push(500);
console.log(boundTotal());                // 5480
Return Value bind dan Subtotal yang Ditambahkan Belakangan
cart.total.bind(cart)Function baru,this = cartBukan cart.total=== bernilai falseboundTotal()bernilai 4980cart.items.push(500)Panggil lagiboundTotal()Membaca items cartsaat itu jugaMengembalikan 5480
bind tidak mengubah cart.total, tapi mengembalikan function baru. Yang terkunci bukan value totalnya, melainkan cart itu sendiri.

total tidak menerima argument, tapi kalau kamu mengunci method yang menerima argument, argument yang kamu berikan ke return value-nya diteruskan apa adanya ke method aslinya. Yang dikunci hanya this, jadi argument-nya bisa berbeda di setiap pemanggilan.

Kamu mengambil method penagihan dari sebuah langganan untuk dipakai di tempat lain. Subscription, coffee, dan tea sudah dideklarasikan.

① Masukkan bill milik coffee ke billCoffee dengan this dikunci ke coffee.

② Pakai billCoffee untuk menampilkan tagihan 3 bulan.

③ Buat billTea untuk tea dengan cara yang sama, lalu tampilkan tagihan 6 bulan.

④ Ubah setiap element [1, 12] dengan billCoffee, gabungkan dengan " / ", lalu tampilkan.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Method yang Tetap Jalan Walau Diambil dari Instance — Arrow Function

Bayangkan method yang sama kamu oper ke beberapa tempat di halaman sebagai function yang dipanggil saat tombol diklik, misalnya tombol suka di sebuah artikel. Kalau bind harus ditulis setiap kali dan kamu lupa sekali saja, tombol itu saja yang berhenti dengan TypeError.

Arrow function field (class field yang value awalnya berupa arrow function) dibuat terpisah untuk tiap instance, setiap kali new dipanggil. Di dalam ekspresi value awal sebuah field, this adalah instance yang baru dibuat, sama seperti di constructor, dan arrow function, yang tidak punya this sendiri, memakai this tersebut.

Karena itu, kamu tidak perlu bind saat mengambil lalu memanggilnya.

class LikeButton {
  likes = 0;
  label = `Suka: ${this.likes}`;   // this di ekspresi value awal adalah button yang sedang dibuat
  // Method biasa: this ditentukan oleh baris pemanggilnya
  addLikeMethod() { this.likes += 1; return this.likes; }
  // Arrow function field: memakai this yang sama dengan ekspresi value awal
  addLike = () => { this.likes += 1; return this.likes; };
}
const button = new LikeButton();
console.log(button.label);                     // Suka: 0

// Ambil lalu panggil tanpa bind
const onPress = button.addLike;
console.log(onPress());                        // 1
console.log(onPress());                        // 2
console.log(Object.keys(button).join(", "));   // likes, label, addLike

// Method biasa berhenti dengan TypeError kalau diambil
const onPressMethod = button.addLikeMethod;
// onPressMethod();                            // TypeError
Tempat addLike Mendapat this
Kurung kurawal class LikeButton
Pengisian value awal field (jalan di setiap new)
  • likes = 0 — masuk ke button
  • this di ekspresi value awal labelbutton yang sedang dibuat (sama seperti di constructor)
Di dalam addLike = () => { ... }
  • Tidak punya this sendiri
  • this.likes — membaca value milik button dari luar
  • Dipanggil sebagai onPress() pun, this tetap button
Di dalam addLikeMethod() { ... }
  • this — apa yang ada di kiri titik pada baris pemanggilnya
  • button kalau dipanggil sebagai button.addLikeMethod()
  • undefined kalau diambil lalu dipanggil
this di arrow function adalah this milik proses yang mengisi value awal field. Hanya di addLikeMethod this berubah mengikuti baris pemanggilnya.

addLike muncul di Object.keys(button) karena arrow function field disimpan di instance itu sendiri, sama seperti likes, sedangkan addLikeMethod tidak muncul. Artinya, kalau kamu membuat 100 instance, function addLike juga dibuat sebanyak 100.

Arrow Function di Object Literal Tidak Mendapat Object Itu sebagai this

Kalau kamu menulis const counter = { add: () => { this.likes += 1 } };, this di dalam counter.add() bukan counter. Object literal tidak menentukan this, jadi arrow function-nya memakai this dari luar. Tulis sebagai add() { ... }.

Kamu membuat tabel tombol jumlah barang yang memetakan nama tiap tombol ke function yang dipanggil saat tombol itu ditekan. QuantityStepper, mug, buttons, dan pressed sudah dideklarasikan.

① Definisikan increase, yang menambah jumlah barang sebanyak 1 lalu mengembalikannya, dengan cara yang tetap jalan walaupun diambil dari instance.

② Tulis decrease, yang mengurangi 1, dengan cara yang sama.

③ Panggil function di buttons sesuai urutan pressed, lalu tampilkan jumlah barangnya.

④ Tampilkan hasil buttons.oldPlus() dan jumlah barang mug.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Mengoper ke Kode yang Memanggilnya Nanti — setTimeout

Katakanlah kamu ingin menampilkan pesan "Pesanan diterima" beserta nama pelanggan 100ms setelah pesanan dikonfirmasi. Kalau kamu menulis setTimeout(this.showMessage, 100) di dalam method, pesannya memang muncul 100ms kemudian, tapi nama pelanggannya menjadi undefined.

setTimeout memanggil sendiri function yang diterimanya 100ms kemudian, dan di browser, saat itu this diisi window (object yang disediakan browser, satu untuk setiap halaman). Kalau kamu membungkusnya dengan arrow function, this di () => this.showMessage() adalah this milik method di luarnya, jadi method-nya dipanggil pada instance.

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

class OrderNotice {
  constructor(customerName) { this.customerName = customerName; }
  showMessage() { console.log(`Pesanan diterima, ${this.customerName}`); }

  // Oper method-nya apa adanya. Yang memanggilnya 100ms kemudian adalah setTimeout
  notifyDirect() { setTimeout(this.showMessage, 100); }

  // Bungkus dengan arrow function / kunci dengan bind sebelum dioper
  notifyWithArrow() { setTimeout(() => this.showMessage(), 100); }
  notifyWithBind() { setTimeout(this.showMessage.bind(this), 100); }
}

const notice = new OrderNotice("Budi");
notice.notifyDirect();      // Pesanan diterima, undefined
notice.notifyWithArrow();   // Pesanan diterima, Budi
notice.notifyWithBind();    // Pesanan diterima, Budi
await delay(300);           // Di halaman ini, tunggu sampai semua output muncul
this Saat setTimeout Memanggil Function
setTimeout(this.showMessage)100ms kemudian,showMessage jalanthis diisiwindowPesanan diterima,undefinedsetTimeout(() =>this.showMessage())100ms kemudian,arrow functiondipanggilnotifyWithArrow:this = noticePesanan diterima,Budi
Di kedua baris, setTimeout yang memanggilnya 100ms kemudian. Di dalam arrow function, kamu bisa memanggil method dengan notice di kiri titik.

Baris atas tidak berhenti dengan TypeError karena this-nya bukan undefined seperti pada calcTotal(), melainkan window. window tidak punya customerName, jadi yang tampil adalah undefined. Tabel di bawah merangkum value this untuk setiap cara memanggil yang dibahas di artikel ini.

Cara memanggilValue thisHasil di artikel ini
cart.total()cart di kiri titikMengembalikan 4980
Diambil, lalu calcTotal()undefinedBerhenti dengan TypeError
Return value cart.total.bind(cart)cart yang dioper ke bindMengembalikan 4980
Arrow function field sebagai onPress()button, pemilik fieldJumlah suka bertambah
setTimeout(this.showMessage, 100)window (di browser)Pesanan diterima, undefined
setTimeout(() => this.showMessage(), 100)this milik method luarPesanan diterima, Budi

Dibungkus dengan function pun, this Tetap Hilang

Kalau kamu membungkusnya seperti setTimeout(function () { this.showMessage(); }, 100), this di dalamnya menjadi window, jadi kodenya berhenti dengan TypeError. Console di halaman ini tidak menampilkan error itu sama sekali; Uncaught TypeError hanya muncul di developer tools browser. Bungkus dengan arrow function.

Di resepsionis hotel, kamu menampilkan pemberitahuan kamar siap 100ms kemudian. RoomGuide, budi, siti, dan delay sudah dideklarasikan.

① Oper announce milik budi ke setTimeout apa adanya.

② Untuk announce milik siti, kunci this dulu sebelum mengopernya.

③ Oper announce milik budi dengan membungkusnya dalam arrow function.

④ Tampilkan "Check-in selesai", lalu bandingkan kapan pesan itu muncul dengan ①–③.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Setelah const calc = cart.total;, apa isi this di dalam total saat kamu memanggil calc()?

Soal 2Setelah hanya menulis cart.total.bind(cart);, apa yang terjadi kalau kamu memasukkan cart.total ke variabel lalu memanggilnya?

Soal 3Saat mengoper method ke setTimeout dari dalam method lain, cara mana yang TIDAK menjaga this tetap berisi instance?