جاوااسکریپت - تایپاسکریپت - چگونه به یک ویژگی شیء متغیر از یک شیء متغیر دسترسی پیدا کنیم


من در تلاش برای نوشتن یک تابع که دو ورودی بگیرد، کمی مشکل دارم:

  1. نام یک شیء
  2. نام یک ملک

و مقدار آن ویژگی را برای آن شی چاپ میکند. با این حال، تمام اشیاء ویژگیهای متفاوتی دارند، آنها همه یکسان نیستند.

اشیا اینطور به نظر میرسند:

class object1 {
  get property1() {
    return 'foo';
  }
  get property2() {
    return 'bar';
  }
}

export default new object1();
class object2 {
  get different1() {
    return 'asdf';
  }
  get different2() {
    return 'ghjk';
  }
}

export default new object2();

این چیزی است که تاکنون امتحان کردهام:

import object1 from '..';
import object2 from '..';

getPropertValue(objectName, propertyName) {
  let objects = [object1, object2];
  let index = objects.indexOf(objectName);
  console.log(objects[index][propertyName]);
}

این کار نکرد، مقدار بازگشتی به صورت undefined بود. به نظر میرسد که اندیس درست محاسبه میشود، اما به نظر نمیرسد objects[index][propertyName] به درستی به مقدار شیء دسترسی داشته باشد. هرچند به طور عجیب، وقتی که مورد زیر را امتحان کردم، تقریباً کار کرد:

import object1 from '..';
import object2 from '..';

getPropertValue(objectName, propertyName) {
  let objects = [object1, object2];
  for (let index in objects) {
    console.log(objects[index][propertyName]);
  }
}

در اینجا من واقعاً موفق شدم که مقدار صحیح را چاپ کنم، اما مشکل این است که از آنجایی که من فقط در یک حلقه for روی تمام اشیاء تکرار میکنم، سعی میکند مقدار را برای همه اشیاء چاپ کند، به جای اینکه فقط برای آن شیء که نام شیء با objectName مطابقت دارد چاپ کند. بنابراین مقدار صحیح برای شیء که ویژگیای که سعی دارم به آن دسترسی پیدا کنم را دارد چاپ میکند، اما سپس برای شیء دیگر که آن ویژگی را ندارد undefined میشود.

فرض میکنم میتوانم یک ویژگی به هر شیء اضافه کنم به نام name و چیزی شبیه به این انجام دهم:

getPropertValue(objectName, propertyName) {
  let objects = [object1, object2];
  for (let index in objects) {
    if(objects[index].name == objectName) {
      console.log(objects[index][propertyName]);
    }
  }
}

اما ترجیح میدهم در صورت امکان ویژگیهای غیرضروری به این اشیاء اضافه نکنم.

راه بهتری برای انجام این کار وجود دارد؟

پاسخ شماره ۱

اشیاء میتوانند توسط یک متغیر ارجاع داده شوند، اما یک شیء هیچ ویژگیای از نامگذاری شده بودن ندارد مگر اینکه کد شما بهوضوح آن را به عنوان بخشی از مدل دادههای شما فراهم کند.

به عنوان مثال:

const obj = { abc: 123 }

در اینجا مقدار { abc: 123 } با نام obj نامگذاری نشده است. یک متغیر با شناسه obj وجود دارد که به مقداری که { abc: 123 } است اشاره میکند.

آنچه این در عمل به این معنا است این است که اگر شما تنها یک ارجاع به { abc: 123 } داشته باشید، نمیتوانید بدانید که سایر شناسههای متغیر نیز به این مقدار ارجاع دارند یا خیر.

و شما نمیتوانید مقدار یک متغیر را با شناسه آن به صورت رشته دریافت کنید.

پس این به هیچ وجه نمیتواند جواب دهد:

const foo = 'bar'

function getVar(varName: string) {
  // impossible function implementation here.
}

getVar('foo') // expected return value: 'bar'

اما چیزی که شما میتوانید داشته باشید یک شیء با ویژگیها است. ویژگیها دارای کلیدهایی هستند که رشتههای خاصی هستند.

const objects = { // note the {}, [] is for arrays
  object1,
  object2,
}

که مخفف برای:

const objects = {
  "object1": object1,
  "object2": object2,
}

شما اکنون شیئی دارید که ویژگیهای آن با نامهایی که میخواهید برای اشیاء خود استفاده کنید مطابقت دارند.


در نهایت، بیایید تابع شما را به درستی تایپ کنیم:

// store this outside the function
const objects = {
  object1,
  object2,
}

function getPropertyValue<
  T extends keyof typeof objects
>(
  objectName: T,
  propertyName: keyof typeof objects[T]
) {
  console.log(objects[objectName][propertyName]);
}

این تابع اکنون عمومی است، به این معنی که یک نوع را میپذیرد. در این مورد، نام شیئی را که میخواهید به عنوان T استفاده کنید میپذیرد، که هر keyof شیء objects باشد.

سپس آرگومان propertyName از هر کلیدی که بود استفاده میکند تا بفهمد چه ویژگیهایی باید مجاز باشند، با محدود کردن نوع به ویژگیهای فقط یکی از آن اشیاء.

برای دریافت دادهها، شما به objects با نام یک ویژگی در objects میروید، و سپس دوباره روی نام ویژگی خاص.

طرز استفاده به این صورت است:

getPropertyValue('object1', 'property1') // 'foo'
getPropertyValue('object2', 'different1') // 'asdf'

getPropertyValue('object1', 'different1')
// Type error, 'different1' does not exist on 'object1'

مشاهده زمین بازی

پاسخ شماره ۲

نام اشیاء

همانطور که الکس وین توضیح میدهد: اشیاء فقط «به عنوان بخشی از مدل داده شما» نام خواهند داشت.

یا به عبارت دیگر: نام یک شیء همان چیزی است که شما با آن مرتبط میکنید.

آن ارتباط ممکن است از درون یا از بیرون باشد. مثال:

// Name by self-definition
const someObject = { name: "self-named" };

// Name by association
const nameToObject = { "associated-name": {} };

جستجو بر اساس نام

برای جلوگیری از افزودن ویژگیهای غیرضروری به اشیاءتان، از روش «نامگذاری بر اساس ارتباط» استفاده خواهم کرد:

const objects = { object1, object2 };

یادداشت حاشیهای: در این مقداردهی اولیهی کوتاه شیء، شناسهها بهعنوان نام ویژگیها و ارجاعات آنها بهعنوان مقادیر ویژگیها استفاده میشوند.

با این، پیادهسازی ساده است:

const object1 = new class Object1 {
  get property1() { return "foo"; }
  get property2() { return "bar"; }
};
const object2 = new class Object1 {
  get different1() { return "asdf"; }
  get different2() { return "ghjk"; }
};

const objects = { object1, object2 };

// The implementation
function getPropertyValue(objectName, propertyName) {
  return objects[objectName][propertyName];
}

const value = getPropertyValue("object1", "property2");
console.log(value);

اضافه کردن تایپاسکریپت

برای اضافه کردن تایپینگ مناسب، باید فکر کنیم که خروجی چگونه به ورودیها بستگی دارد. اما همچنین، ورودیها چگونه به یکدیگر بستگی دارند.

یک پیادهسازی ساده ممکن است شبیه این باشد:

function getPropertyValue(
  objectName: "object1" | "object2",
  propertyName: "property1" | "property2" | "different1" | "different2"
): any;

اما این به طور کامل از قابلیتهای تایپاسکریپت استفاده نمیکند. تنها یک زیرمجموعه از انواع اصلی propertyName میتواند بسته به مقدار objectName استفاده شود: propertyName به objectName وابسته است.

از آنجا که یک وابستگی بین پارامترهای تابع وجود دارد، ما به یک پارامتر عمومی نیاز داریم تا آنها را به هم متصل کند.

ما قبلاً میدانیم که کدام مقدار از objectName با کدام نوع مطابقت دارد، به لطف فرهنگ لغت ما. ما فقط نیاز داریم propertyName را به کلیدهای آن نوع محدود کنیم:

type Objects = typeof objects;

function getPropertyValue<
  K extends keyof Objects
>(
  objectName: K,
  propertyName: keyof Objects[K]
);

هنگام فراخوانی این تابع، K به طور پیشفرض از objectName استنتاج خواهد شد و به طور خودکار گزینههای ما برای propertyName را بر اساس تایپ ما محدود میکند.


ضمیمه

نام بهوسیلهٔ شناسه؟

در حالی که متغیرها (به ویژه آنهایی که با const اعلام شدهاند) میتوانند به اشیاء اشاره کنند، شناسه آنها - یعنی نام متغیر - معمولاً به عنوان نام شیء مرجعشده آنها در نظر گرفته نمیشود. به دلایل خوبی!

شما نمیتوانید بهراحتی اشیاء را از طریق یک شناسه جستجو کنید. برای انجام این کار نیاز به استفاده از new Function یا eval() دارید، که به دلیل داشتن مشکلات امنیتی شناخته شدهاند.

بنابراین، لطفاً موارد زیر را به عنوان یک نمایش نه توصیه در نظر بگیرید:

const object1 = { name: "Object-1" };
const object2 = { name: "Object-2" };

const o = getObjectByName("object1");
console.log(o);

function getObjectByName(name) {
  // You should avoid new Function!
  return new Function(`return ${name};`)();
}

for...in، for...of، برای چه؟

چندین حلقه for وجود دارد:

  • حلقهٔ for معمولی (یا ساده) با یک بلوک مقداردهی اولیه، شرط و افزایش.
  • حلقه for...of برای تکرار روی یک شیء قابل تکرار.
  • حلقه for...in برای گردش در خصوصیات قابل شمارش.

اطمینان حاصل کنید که با اینها آشنا شوید!

نکته: در بسیاری از موارد میتوانید به جای یک حلقه for...in از Object.keys() یا .entries() استفاده کنید، که از سردرگمی با حلقههای for...of جلوگیری میکند.

آلودگی فضای نام!

ما نمیخواهیم برای هر فراخوانی تابع اشیاء را نمونهسازی کنیم، که این به این معناست که باید خارج از آن تابع باشد.

اما انتقال سادهلوحانه آن به خارج، فضای نام را آلوده میکند.

در عوض، میتوانیم از یک closure استفاده کنیم تا فقط یک بار آن را نمونهسازی کنیم، اما آن را از فضای نام سراسری مخفی نگه داریم:

// Mock imports
const object1 = { property1: "foo", property2: "bar" };
const object2 = { different1: "asdf", different2: "ghjk" };

const getPropertyValue = (() => {
  const objects = { object1, object2 };
  
  return function(objectName, propertyName) {
    return objects[objectName][propertyName];
  };
})();

const value = getPropertyValue("object1", "property2");
console.log(value);
console.log(objects);