جاوا اسکریپت: معنی این چیست؟

فهمیدن مقدار this در جاوا اسکریپت می تواند مشکل باشد، در اینجا نحوه انجام آن آمده است…

جاوا اسکریپت this موضوع بسیاری از جوک‌ها است، و این به این دلیل است که، خوب، بسیار پیچیده است. با این حال، من دیده ام که توسعه دهندگان کارهای بسیار پیچیده تر و مختص دامنه را انجام می دهند تا از برخورد با این this جلوگیری کنند. اگر در this مورد مطمئن نیستید، امیدواریم این کمک کند. این this من است.

من می خواهم با خاص ترین موقعیت شروع کنم و با کم خاص ترین وضعیت پایان دهم. این مقاله تا حدودی شبیه یک if (…) … else if () … else if (…) … , بنابراین می توانید مستقیماً به اولین بخش بروید که با کد مورد نظر مطابقت دارد.

  1. اگر تابع به عنوان تابع فلش تعریف شده باشد
  2. در غیر این صورت، اگر تابع/کلاس با new فراخوانی شود
  3. در غیر این صورت، اگر تابع دارای 'bound' this مقدار باشد
  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);
  };
}

این الگو هنگام استفاده از روش‌های نمونه به عنوان شنونده رویداد در مؤلفه‌ها (مانند مؤلفه‌های 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();

در غیر این صورت، اگر تابع دارای 'bound' باشد، this مقدار:

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 روی مقدار دیگری تنظیم شده است و استفاده از آن می تواند منجر به درک کدهای دشوار شود:

نکن
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 برای بررسی.