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

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

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

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

  1. หากฟังก์ชันได้รับการกำหนดเป็นฟังก์ชันลูกศร
  2. หรือหากเรียกใช้ฟังก์ชัน/คลาสด้วย new
  3. หรือหากฟังก์ชันมีค่า this "bound"
  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 ไว้ในเวลาโทร ให้ทําดังนี้

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