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

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

เจค อาร์ชิบาลด์
เจค อาร์ชิบาลด์

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

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

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

รูปแบบนี้มีประโยชน์มากเมื่อใช้เมธอดอินสแตนซ์เป็น Listener เหตุการณ์ในคอมโพเนนต์ (เช่น คอมโพเนนต์รีแอ็กชันหรือคอมโพเนนต์เว็บ)

การดำเนินการข้างต้นอาจดูเหมือนว่ามีการละเมิดกฎ "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 เป็นค่าอื่นตามบางสิ่ง เช่น Listener เหตุการณ์ DOM และการใช้ตัวเลือกนี้อาจทำให้เกิดโค้ดที่เข้าใจยาก

ไม่ควรทำ
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 คุณอาจเคยเห็น Gotcha นี้เมื่อลองสิ่งต่อไปนี้

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 ที่ร่วมรีวิว