this และ Context
this คืออะไร?
หัวข้อที่มีชื่อว่า “this คืออะไร?”this เป็น keyword พิเศษที่อ้างถึง execution context — object ที่ถือเป็น receiver ของการเรียกฟังก์ชันปัจจุบัน ต่างจากภาษาส่วนใหญ่ this ใน JavaScript ไม่ถูก bind เมื่อนิยาม แต่ถูกกำหนด ณ เวลาที่เรียก โดยวิธีที่ฟังก์ชันถูก invoke
this ในรูปแบบการเรียกต่างๆ
หัวข้อที่มีชื่อว่า “this ในรูปแบบการเรียกต่างๆ”// 1. Method call — this is the object before the dotconst 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 globalThisfunction whoAmI() { return typeof this; // 'undefined' in strict mode}
// 3. Constructor call — this is the newly created objectfunction Person(name) { this.name = name;}const p = new Person('Bob');console.log(p.name); // 'Bob'การสูญเสีย this
หัวข้อที่มีชื่อว่า “การสูญเสีย this”ปัญหาที่พบบ่อยที่สุด: การแยก 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); },};call, apply และ bind
หัวข้อที่มีชื่อว่า “call, apply และ bind”ทั้งสาม method นี้ให้คุณ กำหนด this อย่างชัดเจน
function introduce(greeting, punctuation) { return greeting + ', I am ' + this.name + punctuation;}
const obj = { name: 'Carol' };
// call — pass arguments individuallyintroduce.call(obj, 'Hello', '!'); // 'Hello, I am Carol!'
// apply — pass arguments as an arrayintroduce.apply(obj, ['Hey', '.']); // 'Hey, I am Carol.'
// bind — returns a NEW function with this permanently boundconst boundIntroduce = introduce.bind(obj);boundIntroduce('Hi', '?'); // 'Hi, I am Carol?'Arrow functions และ this
หัวข้อที่มีชื่อว่า “Arrow functions และ this”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 โดยอัตโนมัติ
Demo ที่รันได้
หัวข้อที่มีชื่อว่า “Demo ที่รันได้”ตาราง this แบบครบ
หัวข้อที่มีชื่อว่า “ตาราง this แบบครบ”รูปแบบการเรียกหกแบบครอบคลุม 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 อยู่ใน method | this เดียวกับที่อยู่ใน 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"]
Demo ด้านบนครอบคลุม method call และ detached call ไปแล้ว ส่วนนี้ครอบคลุมอีกสามรูปแบบที่มักทำให้ผู้ review แปลกใจที่สุด: arrow ที่ nest อยู่ใน method, class field arrow และ regular function ที่ส่งให้ setTimeout
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
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ให้นึกภาพ 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
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| 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 arrow | Auto-bound ครั้งเดียว ปลอดภัยที่จะส่งเป็น event handler หรือ callback โดยตรง | สร้างฟังก์ชัน instance ใหม่ต่อ object instance หนึ่งตัว ไม่ได้แชร์บน prototype (ต้นทุน memory เล็กน้อยเมื่อมีหลาย instance) |
.bind(obj) | ทำให้ this ที่ถูกต้องถาวรและ override ไม่ได้แม้จะโดนเผลอ | สร้าง function object ใหม่ทุกครั้งที่เรียก — หลีกเลี่ยงการเรียก .bind() ใน loop ที่ทำงานบ่อย |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “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
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”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 เสมอไม่ว่าจะถูกส่งไปที่ไหน