ข้ามไปยังเนื้อหา

this และ Context

this เป็น keyword พิเศษที่อ้างถึง execution context — object ที่ถือเป็น receiver ของการเรียกฟังก์ชันปัจจุบัน ต่างจากภาษาส่วนใหญ่ this ใน JavaScript ไม่ถูก bind เมื่อนิยาม แต่ถูกกำหนด ณ เวลาที่เรียก โดยวิธีที่ฟังก์ชันถูก invoke

// 1. Method call — this is the object before the dot
const user = {
name: 'Alice',
greet: function() {
return 'Hi, I am ' + this.name;
},
};
console.log(user.greet()); // 'Hi, I am Alice'
// 2. Plain function call — this is undefined (strict mode) or globalThis
function whoAmI() {
return typeof this; // 'undefined' in strict mode
}
// 3. Constructor call — this is the newly created object
function Person(name) {
this.name = name;
}
const p = new Person('Bob');
console.log(p.name); // 'Bob'

ปัญหาที่พบบ่อยที่สุด: การแยก method ออกจาก object ทำให้ binding หาย

const timer = {
label: 'tick',
start: function() {
// After 0 ms, fn is called as a plain function — this.label is undefined
setTimeout(function() {
console.log(this.label); // undefined — 'this' is not timer
}, 0);
},
};

ทั้งสาม method นี้ให้คุณ กำหนด this อย่างชัดเจน

function introduce(greeting, punctuation) {
return greeting + ', I am ' + this.name + punctuation;
}
const obj = { name: 'Carol' };
// call — pass arguments individually
introduce.call(obj, 'Hello', '!'); // 'Hello, I am Carol!'
// apply — pass arguments as an array
introduce.apply(obj, ['Hey', '.']); // 'Hey, I am Carol.'
// bind — returns a NEW function with this permanently bound
const boundIntroduce = introduce.bind(obj);
boundIntroduce('Hi', '?'); // 'Hi, I am Carol?'
JavaScript

Arrow functions ไม่มี this ของตัวเอง แต่ inherit this จาก lexical enclosing scope — function หรือ module scope รอบนอกที่จุดที่เขียน

const timer2 = {
label: 'tick',
start: function() {
// Arrow function inherits 'this' from start() — which is timer2
setTimeout(() => {
console.log(this.label); // 'tick' — correctly bound
}, 0);
},
};

ทำให้ arrow functions เป็นตัวเลือกที่เหมาะสมสำหรับ callbacks ภายใน methods

<script> ธรรมดารันแบบ sloppy mode โดย default ซึ่งการเรียก this ที่ detached จะ fallback ไปที่ global object เงียบๆ แทนที่จะ throw — demo ด้านล่างเปิด strict mode อย่างชัดเจน เพื่อให้การเรียกแบบ detached fail เหมือนที่จะเกิดขึ้นจริงใน ES module หรือ class ซึ่งทั้งคู่เป็น strict mode โดยอัตโนมัติ

JavaScript

รูปแบบการเรียกหกแบบครอบคลุม this bug แทบทั้งหมดที่คุณจะเจอในโค้ดจริง:

รูปแบบการเรียกthis คืออะไรสูญเสีย binding ไหม?
obj.method()obj — object ก่อนจุดไม่
const fn = obj.method; fn() (detached)undefined ใน strict mode (throw เมื่อเข้าถึง); global object ใน sloppy modeใช่
Arrow function ที่ nest อยู่ใน methodthis เดียวกับที่อยู่ใน method รอบนอก — inherit มา ไม่ใช่ของตัวเองไม่
Class field arrow (prop = () => { ... })instance นั้นอย่างถาวร — bind ครั้งเดียวตอน constructไม่
Regular function ที่ส่งให้ setTimeout/array callbackไม่ใช่ object ที่คุณคาดไว้ — timer object หรือ undefined ไม่เคยเป็น object ที่มัน “อยู่บน”ใช่
fn.bind(obj) / .call(obj) / .apply(obj)obj อย่างชัดเจน บังคับไม่ว่าผลลัพธ์จะถูกเรียกทีหลังอย่างไรไม่ (นั่นคือจุดประสงค์)
flowchart TD
  A["How is the function called?"] --> B["obj.method() -- this = obj"]
  A --> C["detached: const fn = obj.method; fn() -- this LOST"]
  A --> D["arrow nested in a method -- this inherited from enclosing scope"]
  A --> E["class field arrow -- this = instance, bound at construction"]
  A --> F["passed to setTimeout / array callback as a regular function -- this LOST"]
  A --> G["fn.bind(obj) / .call(obj) / .apply(obj) -- this forced to obj"]
รูปแบบการเรียกไหนที่รักษา this ไว้ และแบบไหนที่สูญเสียไป

Demo ด้านบนครอบคลุม method call และ detached call ไปแล้ว ส่วนนี้ครอบคลุมอีกสามรูปแบบที่มักทำให้ผู้ review แปลกใจที่สุด: arrow ที่ nest อยู่ใน method, class field arrow และ regular function ที่ส่งให้ setTimeout

Node.js
Output (verified on Node 24)
detached class-field arrow still works: 1
nested arrow this.label: inherited-arrow
regular setTimeout callback this.label: undefined
arrow setTimeout callback this.label: tick

สังเกตลำดับใน output: setTimeout callback ทั้งสองตัวรันหลังจากโค้ด synchronous (รวมถึง log ของ class-field arrow) จบไปแล้วเสมอ — นี่คือ event loop ที่ defer macrotask callback ของมันไว้ ซึ่งอธิบายลึกในโมดูล Event Loop & Async

ให้นึกภาพ this เป็นพารามิเตอร์พิเศษที่มองไม่เห็นซึ่ง JavaScript เติมให้ ณ จุดที่เรียก ไม่ใช่ ณ จุดที่นิยาม — เหมือน argument แรกที่ไม่มี label ที่ทุก regular function ได้รับมาแบบลับๆ obj.method() เติมมันด้วย obj fn() เปล่าๆ เติมด้วย undefined (strict) หรือ global object (sloppy) fn.call(obj)/fn.apply(obj)/fn.bind(obj) ให้คุณเติมมันเองได้ Arrow function คือข้อยกเว้นเดียว: มันไม่ได้รับพารามิเตอร์ที่มองไม่เห็นนี้เลย — arrow function อ่าน this ตามตัวอักษรจาก scope ที่มันถูกเขียนไว้ เหมือนตัวแปรอื่นๆ ที่มัน close over

ChoiceBenefitCost
Regular function เป็น methodได้ this แบบ dynamic ของตัวเอง ใช้ซ้ำได้กับ receiver object ต่างกัน (fn.call(otherObj))สูญเสีย this ได้ง่ายเมื่อแยกฟังก์ชันออกมาหรือส่งเป็น callback
Arrow function เป็น callbackสูญเสีย this ไม่ได้ — เพราะไม่เคยเป็นของตัวเองตั้งแต่แรกre-target ด้วย call/apply/bind ไม่ได้; ไม่เหมาะเป็น object method เอง
Class field arrowAuto-bound ครั้งเดียว ปลอดภัยที่จะส่งเป็น event handler หรือ callback โดยตรงสร้างฟังก์ชัน instance ใหม่ต่อ object instance หนึ่งตัว ไม่ได้แชร์บน prototype (ต้นทุน memory เล็กน้อยเมื่อมีหลาย instance)
.bind(obj)ทำให้ this ที่ถูกต้องถาวรและ override ไม่ได้แม้จะโดนเผลอสร้าง function object ใหม่ทุกครั้งที่เรียก — หลีกเลี่ยงการเรียก .bind() ใน loop ที่ทำงานบ่อย
  • “Arrow functions เป็นแค่ syntax สั้นกว่าของ regular functions” มันเปลี่ยน semantics ด้วย: ไม่มี this ของตัวเอง, ไม่มี arguments ของตัวเอง และใช้เป็น constructor ไม่ได้ (new arrowFn() throw)
  • “ถ้าฟังก์ชันถูกนิยามใน object literal this จะอ้างถึง object นั้นเสมอ” จริงเฉพาะฟังก์ชันที่เขียนด้วย method() {} หรือ method: function() {} shorthand และ ถูกเรียกเป็น obj.method() เท่านั้น ฟังก์ชันเดียวกันที่ถูกแยกออกมาแล้วเรียกเดี่ยวๆ จะสูญเสีย binding นั้นไปทั้งหมด
  • “bind รันฟังก์ชันทันที” fn.bind(obj) คืนฟังก์ชันใหม่ทั้งหมดโดยไม่เรียก fn คุณยังต้อง invoke ผลลัพธ์เอง
  • “Arrow class field ไม่มีต้นทุนเทียบกับ prototype method” มัน bind ครั้งเดียวต่อ instance ซึ่งเป็นสิ่งที่คุณต้องการสำหรับ callback พอดี — แต่มันหมายความว่า N instance สร้าง function object แยกกัน N ตัว ต่างจาก prototype method ที่แชร์กันทุก instance

this bug ที่พบบ่อยที่สุดในโค้ดที่ AI สร้างคือการส่ง object method ตรงๆ เป็น callback — setTimeout(obj.method, 1000), array.map(obj.method), emitter.on('event', obj.method) — ทั้งหมดนี้แยก method ออกจาก obj ตรงจุดที่มันถูกส่งไป เหมือน detached-call bug แบบคลาสสิกทุกประการ เพียงแต่ห่างออกมาอีกหนึ่งชั้นจนไม่ชัดเจน

🐛 หา bug ในโค้ดนี้

class Poller {
constructor(url) {
this.url = url;
this.attempts = 0;
}
poll() {
this.attempts += 1;
console.log('polling', this.url, 'attempt', this.attempts);
}
start() {
setInterval(this.poll, 1000); // looks fine — poll is "the method"
}
}
new Poller('https://example.com').start();
เฉลย

setInterval(this.poll, 1000) ส่งฟังก์ชัน poll เดี่ยวๆ ที่แยกออกจาก instance ของ Poller — เป็น bug เดียวกับ const fn = obj.method; fn() ทุกประการ พอ timer ทำงาน poll จะรันโดยมี this เป็น undefined (class body เป็น strict) ดังนั้น this.attempts += 1 จะ throw TypeError: Cannot read properties of undefined วิธีแก้คือรักษา binding ไว้อย่างชัดเจน: setInterval(this.poll.bind(this), 1000), setInterval(() => this.poll(), 1000) หรือประกาศ poll เป็น class field arrow (poll = () => { ... }) เพื่อให้มันถูก bind กับ instance เสมอไม่ว่าจะถูกส่งไปที่ไหน

this ของ regular function (non-arrow) ใน JavaScript ถูกกำหนดเมื่อไหร่?
fn.bind(obj) return อะไร?
ทำไม arrow functions จึงทำงานได้ดีเป็น callbacks ภายใน class methods?
เกิดอะไรขึ้นเมื่อ timer ทำงาน?
class Poller {
  constructor() { this.count = 0; }
  poll() { this.count += 1; return this.count; }
  start() { return setTimeout(this.poll, 0); }
}
const p = new Poller();
p.start();
โค้ดนี้พิมพ์อะไร และทำไมมันถึงต่างจาก detached method ทั่วไป?
class Counter {
  count = 0;
  inc = () => { this.count += 1; return this.count; };
}
const c = new Counter();
const detached = c.inc;
console.log(detached());