การหาค่าของ this
ใน JavaScript อาจเป็นเรื่องยาก มาดูวิธีกัน
this
ของ JavaScript มักถูกใช้เป็นมุกตลก เนื่องจากมีความซับซ้อน
อย่างไรก็ตาม เราเคยเห็นนักพัฒนาซอฟต์แวร์ทำสิ่งต่างๆ ที่ซับซ้อนกว่ามากและเจาะจงโดเมนเพื่อหลีกเลี่ยงการจัดการกับ this
นี้ หากไม่แน่ใจเกี่ยวกับ this
เราหวังว่าข้อมูลนี้จะช่วยคุณได้ นี่คือคู่มือ this
ของฉัน
เราจะเริ่มด้วยสถานการณ์ที่เฉพาะเจาะจงที่สุด และจบด้วยสถานการณ์ที่เฉพาะเจาะจงน้อยที่สุด บทความนี้เปรียบเสมือน if (…) … else if () … else if (…) …
ขนาดใหญ่ คุณจึงข้ามไปยังส่วนแรกๆ ที่ตรงกับโค้ดที่กําลังดูได้
- หากฟังก์ชันได้รับการกำหนดเป็นฟังก์ชันลูกศร
- หรือหากเรียกใช้ฟังก์ชัน/คลาสด้วย
new
- หรือหากฟังก์ชันมีค่า
this
ที่ "เชื่อมโยง" - หรือหากตั้งค่า
this
เป็น call-time - หรือหากเรียกฟังก์ชันผ่านออบเจ็กต์หลัก (
parent.func()
) - หรือหากฟังก์ชันหรือขอบเขตหลักอยู่ในโหมดเข้มงวด
- มิเช่นนั้น
หากกำหนดฟังก์ชันเป็นฟังก์ชันลูกศร ให้ทำดังนี้
const arrowFunction = () => {
console.log(this);
};
ในกรณีนี้ ค่าของ this
จะเหมือนกันกับ this
ในสโคปหลักเสมอ
const outerThis = this;
const arrowFunction = () => {
// Always logs `true`:
console.log(this === outerThis);
};
ฟังก์ชันลูกศรมีประโยชน์มากเนื่องจากค่าภายในของ this
จะเปลี่ยนแปลงไม่ได้ แต่จะเหมือนกันเสมอกับ this
ภายนอก
ตัวอย่างอื่นๆ
เมื่อใช้ฟังก์ชันลูกศร ค่าของ this
จะเปลี่ยนไม่ได้โดยใช้ bind
// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();
เมื่อใช้ฟังก์ชันลูกศร ค่าของ this
จะเปลี่ยนไม่ได้โดยใช้ call
หรือ apply
// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});
เมื่อใช้ฟังก์ชันลูกศร ค่าของ this
จะเปลี่ยนไม่ได้โดยการเรียกใช้ฟังก์ชันในฐานะสมาชิกของออบเจ็กต์อื่น
const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();
เมื่อใช้ฟังก์ชันลูกศร ค่าของ this
จะเปลี่ยนแปลงไม่ได้โดยการเรียกใช้ฟังก์ชันเป็นคอนสตรัคเตอร์ ดังนี้
// TypeError: arrowFunction is not a constructor
new arrowFunction();
เมธอดอินสแตนซ์ที่ "เชื่อมโยง"
หากต้องการตรวจสอบว่า this
อ้างอิงอินสแตนซ์ของคลาสเสมอเมื่อใช้เมธอดอินสแตนซ์ วิธีที่ดีที่สุดคือใช้ฟังก์ชันลูกศรและฟิลด์คลาส ดังนี้
class Whatever {
someMethod = () => {
// Always the instance of Whatever:
console.log(this);
};
}
รูปแบบนี้มีประโยชน์อย่างยิ่งเมื่อใช้เมธอดอินสแตนซ์เป็นผู้ฟังเหตุการณ์ในคอมโพเนนต์ (เช่น คอมโพเนนต์ React หรือคอมโพเนนต์ของเว็บ)
ตัวอย่างข้างต้นอาจดูเหมือนว่าละเมิดกฎ "this
จะเหมือนกับ this
ในสโกปหลัก" แต่จะเริ่มสมเหตุสมผลขึ้นเมื่อคุณคิดว่าฟิลด์คลาสเป็นรูปแบบคำสั่งที่ช่วยให้เขียนโค้ดได้ง่ายขึ้นสำหรับการตั้งค่าต่างๆ ในคอนสตรัคเตอร์
class Whatever {
someMethod = (() => {
const outerThis = this;
return () => {
// Always logs `true`:
console.log(this === outerThis);
};
})();
}
// …is roughly equivalent to:
class Whatever {
constructor() {
const outerThis = this;
this.someMethod = () => {
// Always logs `true`:
console.log(this === outerThis);
};
}
}
รูปแบบอื่นๆ เกี่ยวข้องกับการเชื่อมโยงฟังก์ชันที่มีอยู่ในตัวสร้าง หรือการกำหนดฟังก์ชันในตัวสร้าง หากใช้ฟิลด์คลาสไม่ได้ด้วยเหตุผลบางประการ การกำหนดฟังก์ชันในคอนสตรัคเตอร์ก็เป็นทางเลือกที่เหมาะสม
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
หรือหากเรียกใช้ฟังก์ชัน/คลาสด้วย new
ให้ทำดังนี้
new Whatever();
บรรทัดด้านบนจะเรียก Whatever
(หรือฟังก์ชันคอนสตรัคเตอร์หากเป็นคลาส) โดยตั้งค่า this
เป็นผลลัพธ์ของ Object.create(Whatever.prototype)
class MyClass {
constructor() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
}
// Logs `true`:
new MyClass();
เช่นเดียวกับตัวสร้างรูปแบบเก่า
function MyClass() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
// Logs `true`:
new MyClass();
ตัวอย่างอื่นๆ
เมื่อเรียกใช้ด้วย new
ค่าของ this
จะเปลี่ยนแปลงไม่ได้ด้วย bind
const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();
เมื่อเรียกใช้ด้วย new
ระบบจะเปลี่ยนค่าของ this
ไม่ได้โดยการเรียกใช้ฟังก์ชันในฐานะสมาชิกของออบเจ็กต์อื่น
const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();
หรือหากฟังก์ชันมีค่า this
"bound" ให้ทำดังนี้
function someFunction() {
return this;
}
const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);
เมื่อใดก็ตามที่มีการเรียกใช้ boundFunction
ค่า this
จะเป็นออบเจ็กต์ที่ส่งไปยัง bind
(boundObject
)
// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);
ตัวอย่างอื่นๆ
เมื่อเรียกใช้ฟังก์ชันที่เชื่อมโยง ค่าของ this
จะเปลี่ยนไม่ได้ด้วย call
หรือ
apply
// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
เมื่อเรียกใช้ฟังก์ชันที่เชื่อมโยง ค่าของ this
จะเปลี่ยนแปลงไม่ได้โดยการเรียกใช้ฟังก์ชันในฐานะสมาชิกของออบเจ็กต์อื่น
const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);
หรือหากตั้งค่า this
เป็น call-time
function someFunction() {
return this;
}
const someObject = {hello: 'world'};
// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);
ค่าของ this
คือออบเจ็กต์ที่ส่งไปยัง call
/apply
แต่ this
ได้รับการตั้งค่าเป็นค่าอื่นโดยสิ่งต่างๆ เช่น โปรแกรมรับฟังเหตุการณ์ DOM และการใช้ this
อาจส่งผลให้โค้ดเข้าใจยาก
element.addEventListener('click', function (event) {
// Logs `element`, since the DOM spec sets `this` to
// the element the handler is attached to.
console.log(this);
});
เราหลีกเลี่ยงการใช้ this
ในกรณีเช่นนี้ และจะใช้รูปแบบต่อไปนี้แทน
element.addEventListener('click', (event) => {
// Ideally, grab it from a parent scope:
console.log(element);
// But if you can't do that, get it from the event object:
console.log(event.currentTarget);
});
หรือหากมีการเรียกใช้ฟังก์ชันผ่านออบเจ็กต์หลัก (parent.func()
) ให้ทำดังนี้
const obj = {
someMethod() {
return this;
},
};
// Logs `true`:
console.log(obj.someMethod() === obj);
ในกรณีนี้ ระบบจะเรียกใช้ฟังก์ชันในฐานะสมาชิกของ obj
ดังนั้น this
จะเป็น obj
การดำเนินการนี้จะเกิดขึ้นเมื่อถึงเวลาเรียกใช้ ดังนั้นลิงก์จะใช้งานไม่ได้หากเรียกใช้ฟังก์ชันโดยไม่มีออบเจ็กต์หลัก หรือมีออบเจ็กต์หลักอื่น
const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);
const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);
someMethod() === obj
เป็นเท็จเนื่องจาก someMethod
ไม่ได้เรียกเป็นสมาชิกของ obj
คุณอาจพบปัญหานี้เมื่อพยายามดำเนินการบางอย่าง เช่น
const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');
การดำเนินการนี้ใช้งานไม่ได้เนื่องจากการติดตั้งใช้งาน querySelector
จะดูค่า this
ของตัวเองและคาดหวังว่าจะเป็นโหนด DOM ประเภทหนึ่ง และการดำเนินการข้างต้นจะตัดการเชื่อมต่อนั้น วิธีดำเนินการข้างต้นอย่างถูกต้อง
const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);
เกร็ดความรู้: API บางรายการไม่ได้ใช้ this
ภายใน มีการเปลี่ยนแปลงเมธอดคอนโซลอย่าง console.log
เพื่อหลีกเลี่ยงการอ้างอิง this
ดังนั้น log
จึงไม่จำเป็นต้องเชื่อมโยงกับ console
หรือหากฟังก์ชันหรือขอบเขตหลักอยู่ในโหมดที่เข้มงวด
function someFunction() {
'use strict';
return this;
}
// Logs `true`:
console.log(someFunction() === undefined);
ในกรณีนี้ ค่าของ this
จะเป็นค่าที่ไม่ระบุ คุณไม่จำเป็นต้องใช้ 'use strict'
ในฟังก์ชันหากขอบเขตหลักอยู่ในโหมดที่เข้มงวด (และโมดูลทั้งหมดอยู่ในโหมดที่เข้มงวด)
กรณีอื่น:
function someFunction() {
return this;
}
// Logs `true`:
console.log(someFunction() === globalThis);
ในกรณีนี้ ค่าของ this
จะเหมือนกับ globalThis
ในที่สุด
เพียงเท่านี้ก็เรียบร้อยแล้ว นั่นคือทั้งหมดที่เราทราบเกี่ยวกับ this
ช่วงถามและตอบ มีสิ่งใดที่เราพลาดไปไหม คุณสามารถทวีตถึงเราได้
ขอขอบคุณ Mathias Bynens, Ingvar Stepanyan และ Thomas Steiner ที่ตรวจสอบ