JavaScript: ความหมายของการทำงาน

การหาค่าของ this ใน JavaScript อาจเป็นเรื่องยาก มาดูวิธีกัน

this ของ JavaScript มักถูกใช้เป็นมุกตลก เนื่องจากมีความซับซ้อน อย่างไรก็ตาม เราเคยเห็นนักพัฒนาซอฟต์แวร์ทำสิ่งต่างๆ ที่ซับซ้อนกว่ามากและเจาะจงโดเมนเพื่อหลีกเลี่ยงการจัดการกับ this นี้ หากไม่แน่ใจเกี่ยวกับ this เราหวังว่าข้อมูลนี้จะช่วยคุณได้ นี่คือคู่มือ this ของฉัน

เราจะเริ่มด้วยสถานการณ์ที่เฉพาะเจาะจงที่สุด และจบด้วยสถานการณ์ที่เฉพาะเจาะจงน้อยที่สุด บทความนี้เปรียบเสมือน if (…) … else if () … else if (…) … ขนาดใหญ่ คุณจึงข้ามไปยังส่วนแรกๆ ที่ตรงกับโค้ดที่กําลังดูได้

  1. หากฟังก์ชันได้รับการกำหนดเป็นฟังก์ชันลูกศร
  2. หรือหากเรียกใช้ฟังก์ชัน/คลาสด้วย new
  3. หรือหากฟังก์ชันมีค่า this ที่ "เชื่อมโยง"
  4. หรือหากตั้งค่า this เป็น call-time
  5. หรือหากเรียกฟังก์ชันผ่านออบเจ็กต์หลัก (parent.func())
  6. หรือหากฟังก์ชันหรือขอบเขตหลักอยู่ในโหมดเข้มงวด
  7. มิเช่นนั้น

หากกำหนดฟังก์ชันเป็นฟังก์ชันลูกศร ให้ทำดังนี้

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 ที่ตรวจสอบ