JavaScript
Object
call / apply
เรียนรู้วิธีกำหนด this เองด้วย explicit binding ผ่าน .call() และ .apply() — เรียกฟังก์ชันทันทีพร้อมระบุว่า this เป็นใคร และเข้าใจความต่างของการส่ง arguments แบบแยกทีละตัวกับแบบ array
ทำไมต้องควบคุม this เอง
จากบท `this` เราเห็นว่า `this` เปลี่ยนตาม caller — `obj.greet()` ได้ `this` = `obj` แต่ `fn()` แบบ standalone ได้ `this` = `window` จากบท `Arrow function issue` เราเห็นว่าใช้ arrow function ใน callback เป็นวิธีหนึ่งที่รักษา `this` ไว้ได้ เพราะ arrow function สืบทอด `this` จาก scope ที่เขียน แต่ยังมีอีกวิธีที่ทรงพลังกว่า — การ **กำหนดค่า `this` เองโดยตรง** ไม่ต้องขึ้นอยู่กับ caller หรือ lexical scope เราเรียกว่า **explicit binding** JavaScript มี built-in method 3 ตัวสำหรับ explicit binding: `.call()`, `.apply()`, และ `.bind()` — บทนี้จะสอน `.call()` และ `.apply()` ส่วน `.bind()` อยู่ในบทถัดไป
fn.call(thisArg, arg1, arg2, ...) — เรียกทันทีพร้อมกำหนด this
`.call()` เป็น method ที่ติดมากับทุกฟังก์ชัน ใช้สำหรับ **เรียกฟังก์ชันทันที** พร้อมกำหนดว่า `this` จะเป็นอะไร รูปแบบ: `fn.call(thisArg, arg1, arg2, ...)` - `thisArg` — object ที่ต้องการให้เป็น `this` - `arg1, arg2, ...` — arguments ปกติของฟังก์ชัน ส่งแยกทีละตัว ข้อแตกต่างจาก implicit binding (`obj.fn()`): `.call()` ทำให้เรากำหนด `this` ได้โดยไม่ต้องให้ฟังก์ชันเป็น method ของ object นั้น
function introduce() {
return "ฉันคือ " + this.name + " อายุ " + this.age + " ปี";
}
const alice = { name: "Alice", age: 25 };
const bob = { name: "Bob", age: 30 };
// ใช้ call เพื่อบอกว่า this คือ alice
console.log(introduce.call(alice)); // "ฉันคือ Alice อายุ 25 ปี"
// ใช้ call กับ bob — ฟังก์ชันเดียวกัน แต่ this เปลี่ยน
console.log(introduce.call(bob)); // "ฉันคือ Bob อายุ 30 ปี"
// ไม่มี implicit binding — introduce ไม่ใช่ method ของ alice หรือ bob
// แต่ call ทำให้ this เป็น alice หรือ bob ได้ตามต้องการargument ตัวแรกของ .call() คือ `thisArg` เสมอ — arguments จริงของฟังก์ชันตามมาเป็นตัวที่ 2, 3, ...
// .call() ส่ง arguments ได้เหมือนเรียกฟังก์ชันปกติ — แค่เลื่อนไปทางขวา
function greet(greeting, punctuation) {
return greeting + ", " + this.name + punctuation;
}
const user = { name: "Alice" };
// thisArg = user, ตามด้วย arguments ทีละตัว
console.log(greet.call(user, "สวัสดี", "!")); // "สวัสดี, Alice!"
console.log(greet.call(user, "Hello", ".")); // "Hello, Alice."
// เทียบกับการเรียกปกติ (ถ้า greet เป็น method ของ user):
// user.greet("สวัสดี", "!") ← implicit binding
// greet.call(user, "สวัสดี", "!") ← explicit binding ได้ผลลัพธ์เหมือนกันUse case: ยืม method จาก object หนึ่งไปใช้กับอีก object
หนึ่งใน use case ที่พบบ่อยของ `.call()` คือการ **ยืม method** จาก object หนึ่งไปใช้กับอีก object หนึ่ง — โดยไม่ต้อง copy method หรือใช้ inheritance แค่มี object ที่มี method อยู่แล้ว กับ object ใหม่ที่มี property ตามที่ method คาดหวัง — `.call()` ก็ทำให้ method ทำงานกับ object ใหม่ได้ทันที
const animal = {
type: "แมว",
color: "ส้ม",
describe: function () {
return "ฉันเป็น " + this.color + " " + this.type;
},
};
const robot = { type: "หุ่นยนต์", color: "เงิน" };
// ใน animal — this = animal
console.log(animal.describe()); // "ฉันเป็น ส้ม แมว"
// ยืม method จาก animal มาใช้กับ robot — this = robot
console.log(animal.describe.call(robot)); // "ฉันเป็น เงิน หุ่นยนต์"
// robot ไม่มี method describe — แต่ call ทำให้ใช้ได้ชั่วคราว
// โดย robot มี property type กับ color ตามที่ describe คาดหวัง`Array.prototype.push.call(obj, ...)` — pattern นี้เจอได้ในโค้ดเก่า ไม่จำเป็นต้องใช้ในโค้ดใหม่ แต่ช่วยให้เข้าใจความสามารถของ `.call()`
// ตัวอย่างจริง — ใช้ call กับ method ของ built-in
// Array.prototype.push.call() — ใช้ push กับ array-like object
const obj = { 0: "a", 1: "b", length: 2 };
// obj ไม่ใช่ array — ไม่มี method push
// แต่ใช้ call ทำให้ push ทำงานกับ obj ได้
Array.prototype.push.call(obj, "c", "d");
console.log(obj); // { 0: "a", 1: "b", 2: "c", 3: "d", length: 4 }
// ไม่ต้องใช้บ่อยในโค้ดสมัยใหม่ — แต่เป็นรูปแบบที่เจอในโค้ดเก่าfn.apply(thisArg, [arg1, arg2, ...]) — เหมือน call แต่ arguments เป็น array
`.apply()` ทำงานเหมือน `.call()` ทุกประการ — ต่างกันแค่วิธีส่ง arguments: - `.call()` — ส่ง arguments แยกทีละตัว (`fn.call(obj, a, b, c)`) - `.apply()` — ส่ง arguments เป็น array (`fn.apply(obj, [a, b, c])`) ในอดีต `.apply()` มีประโยชน์เวลา arguments อยู่ใน array อยู่แล้ว โดยไม่ต้องแกะ แต่ปัจจุบันมี spread operator (`...`) ที่ทำหน้าที่เดียวกันได้
ทั้ง 3 วิธีให้ผลเหมือนกัน — ปัจจุบัน spread `...` เป็นที่นิยมกว่า แต่ `.apply()` ยังเจอในโค้ดเก่า
function sum(a, b, c) {
return a + b + c;
}
const nums = [10, 20, 30];
// .apply() — arguments เป็น array
console.log(sum.apply(null, nums)); // 60
// .call() — arguments แยกทีละตัว
console.log(sum.call(null, nums[0], nums[1], nums[2])); // 60
// ES6+ spread — วิธีสมัยใหม่ (เจอบ่อยกว่า)
console.log(sum(...nums)); // 60// .apply() มี thisArg เหมือน .call()
const person = {
fullName: function (title, suffix) {
return title + " " + this.first + " " + this.last + suffix;
},
};
const user = { first: "สมชาย", last: "ใจดี" };
const args = ["คุณ", "ครับ"];
// apply — arguments เป็น array
console.log(person.fullName.apply(user, args)); // "คุณ สมชาย ใจดีครับ"
// call — arguments แยก
console.log(person.fullName.call(user, "คุณ", "ครับ")); // "คุณ สมชาย ใจดีครับ"
// สมัยใหม่ — แค่ใช้ spread
console.log(person.fullName.call(user, ...args)); // "คุณ สมชาย ใจดีครับ"ตารางเปรียบเทียบ call vs apply vs spread
| เรื่อง | .call() | .apply() | spread (...) |
|---|---|---|---|
| เรียกฟังก์ชันทันที? | ✓ | ✓ | ขึ้นอยู่กับ context |
| กำหนด this ได้? | ✓ — argument ที่ 1 | ✓ — argument ที่ 1 | ✗ — ต้องใช้กับ .call() หรือ .apply() |
| วิธีส่ง arguments | แยกทีละตัว | เป็น array | กระจายจาก array |
| Syntax | fn.call(obj, a, b) | fn.apply(obj, [a, b]) | fn(...[a, b]) |
| ใช้เมื่อไหร่ | รู้ arguments ล่วงหน้า แยกเป็นตัว ๆ | arguments อยู่ใน array อยู่แล้ว | โค้ดสมัยใหม่ — สะดวก อ่านง่าย |
ข้อควรรู้ — thisArg เป็น null หรือ undefined
ถ้าส่ง `null` หรือ `undefined` เป็น `thisArg` ให้กับ `.call()` หรือ `.apply()` — JavaScript จะใช้ default binding แทน (ใน non-strict mode: `this` = `window`, ใน strict mode: `this` = `null`/`undefined`) นี่คือเหตุผลที่เราเห็น `fn.call(null, ...)` ในบางตัวอย่าง — ใช้เมื่อเราไม่สนใจ `this` แค่ต้องการส่ง arguments
สรุป
.call() และ .apply() เป็น explicit binding — เรากำหนด this เองโดยตรง ไม่ต้องขึ้นอยู่กับ caller
- **`.call(thisArg, a, b, ...)`** — เรียกฟังก์ชันทันที กำหนด `this` เอง ส่ง arguments แยกทีละตัว
- **`.apply(thisArg, [a, b, ...])`** — เหมือน `.call()` แต่ส่ง arguments เป็น array
- **Explicit binding** ทำให้เรากำหนด `this` เองได้ โดยไม่ต้องผูกฟังก์ชันเป็น method ของ object
- **ยืม method ได้** — `obj.method.call(otherObj, ...)` ทำให้ method ของ object หนึ่งทำงานกับอีก object หนึ่งได้
- **Spread `...`** เป็นทางเลือกสมัยใหม่แทน `.apply()` — `fn.call(obj, ...args)` หรือ `fn(...args)`
- **บทถัดไป** จะเรียน `.bind()` — explicit binding อีกแบบที่ยังไม่เรียกฟังก์ชันทันที แต่คืนฟังก์ชันใหม่ที่ `this` ถูกตรึงไว้ ใช้บ่อยมากกับ callback