Proxy و کاربردهاش در جاوااسکریپت

یکی از فیچرهایی که در نسخه ES6 به جاوااسکریپت اضافه شد، کانستراکتور Proxy هست. توی این مطلب باهم در مورد Proxy بیشتر یاد میگیریم وکاربردهایی که میتونه در برنامه نویسی روزانه جاوااسکریپتمون داشته باشه رو باهم بررسی میکنیم.

نکته: نمونه کدها رو با ES6 نوشتم. برای استفاده ازش توی پروداکشن، حتما با ترنسپایلری مثل Babel تبدیلش کنید به ES5

Proxy چیکار میکنه؟

طبق تعریف MDN ، پراکسی بهمون اجازه میده یه آبجکت با اینترفیس خاصی که در ادامه توضیح میدم تعریف کنیم تا به کمکش بتونیم عملکردهای اساسی روی دیتا رو کنترل کنیم. به عبارتی دیگه، میتونیم پیدا کردن یه آیتم توی آبجکت، اضافه کردن آیتم جدید به آبجکت، حذف آیتم از آبجکت و … رو کنترل کنیم و براش یه Middleware بنویسیم.

Proxy چطور کار میکنه؟

برای استفاده از Proxy، باید دو پارامتر بهش پاس داده بشه. پارامتر اول آبجکتی هست که میخوایم کنترلش کنیم. یادمون باشه در جاوااسکریپت، Function ها و آرایه ها هم آبجکت محسوب میشن و میشه به عنوان پارامتر اول به Proxy پاسشون داد. پارامتر دوم هندلری هست که طبق متدهاش، کنترلمون روی آبجکت رو پیاده سازی میکنیم.

const proxy = new Proxy(obj, handler)

هندلری که به عنوان پارامتر دوم پاس داده میشه متدهای زیادی رو قبول میکنه که لیست کاملش رو توی لینک MDN میتونید ببینید. دومورد از پرکاربردترین متدهای این هندلر (که توی این مطلب باهاشون کار میکنم)، Set و Get هستن.

Set بهتون اجازه میده فرآیند اضافه شدن آیتم جدید به آبجکت مورد نظر رو کنترل کنید. Get بهتون اجازه میده فرآیند خوندن یه آیتم از آبجکت مورد نظر رو کنترل کنید.

با Proxy چه کارایی میشه کرد؟

اگر یادتون باشه یه متدی توی جاوااسکریپت به صورت آزمایشی اضافه شده بود به اسم Object.observe که الان دیگه از رده خارج شده و در اکثر موتورهای رندر جی اس حذف شده اس. کار این متد این بود که تغییرات روی یه آبجکت رو کنترل میکرد. کد زیر رو به عنوان مثال ببینید:

var obj = {
  foo: 0,
  bar: 1
}
Object.observe(obj, function(changes) {
  console.log(changes);
})

obj.baz = 2
// [{name: 'baz', object: <obj>, type: 'add'}]

زمانی که تلاش میکردیم تا کلید baz رو به آبجکت obj اضافه کنیم، این فرآیند توسط Object.observe دیده میشد و یه لاگ نشون میداد از عملیاتی که انجام شده بود.

[{name: 'baz', object: <obj>, type: 'add'}]

یکی از کارهایی که با Proxy میشه انجام داد Observe کردن آبجکت هاست تا بتونیم زمانی که آیتمی از آبجکت خونده میشه و یا بهش اضافه میشه، مطلعمون کنه.

شبیه سازی Object.observe با استفاده از Proxy

برای مشاهده دائمی تغییرات یه آبجکت کافیه یه هندلر با متدهای Set و Get بسازیم و زمانی که عملیات های خوندن و اضافه کردن به آبجکت انجام میشه، اونو توی کنسول نشون بدیم:

// The object we wanna control
let obj = {}

// Our handler to control object via Proxy
const handler = {
  get(obj, prop) {
    console.log(`Getting property ${prop} from object`)
    // Remember to so the default operation, returning the prop item inside obj
    return obj[prop]
  },
  set(obj, prop, value) {
    console.log(`Setting property ${prop} as ${value} in object`)

    // Do the default operation, set prop as value in obj
    obj[prop] = value

    /*
      Set method must return a value.
      Return `true` to indicate that assignment succeeded
      Return `false` (even a falsy value) to prevent assignment.in `strict mode`, returning false will throw TypeError
    */

    return true
  }
}

// Set the proxy
let proxifiedObj = new Proxy(obj, handler)

// Now to test the handler, Just set some stuff or read props from the obj
proxifiedObj.name = 'Farzad' // Setting property name as Farzad in object
proxifiedObj.name // Getting property name from object

همونطور که توی کد بالا مشخصه، یه هندلر ساختیم و برایش متدهای Set و Get نوشتیم. زمانی که یه آبجکت رو پراکسی میکنیم و یه آیتم ازش رو فراخوانی میکنیم مثل proxifiedObj.name، چون آبجکت proxifiedObj پراکسی شده، مستقیم مقدار .name خونده نمیشه، بلکه متد handler.get خونده میشه. که این متد یه لاگ به کنسول انجام میده و اعلام میکنه که چه آیتمی داره خونده میشه و بعدش مقدار این آیتم یعنی proxifiedObj.name رو ریترن میکنه.

توی متد Set هم نوشتیم، زمانی که میخوایم آیتمی مثل name رو به آبجکتمون اضافه کنیم، به جای اینکه مستقیما این مقدار بهش اساین بشه، متد handler.set فراخوانی میشه. این متد هم لاگ میکنه که آیتم فلان با مقدار فلان قراره با آبجکتمون اضافه بشه.

نکته: به کامنتی که در انتهای متد Set نوشتم دقت کنین. متد Set همیشه باید یه مقدار رو ریترن کنه. MDN توی توضیحاتش میگه این مقدار باید ترحیجا Boolean باشه اما در مثال های دیگه میبینیم که فقط کافیه این مقدار Falsy نباشه. امن ترین راه برای حفظ رفتار پیش فرض و اضافه کردن آیتم به آبجکت، return true هست. اگر هم بخوایم که این اضافه شدن انجام نشه، کافیه یه مقدار Falsy ریترن کنیم.

مقادیر Falsy در جاوااسکریپت: undefined, null, false, 0, ‘‘NaN

نمونه لایو از کد بالا رو توی کدپن گداشتم تا ببینید:

See the Pen Detect Changes in JS Object using ES6 Proxy by Farzad YZ (@farskid) on CodePen.

Deep Freeze کردن آبجکت ها با استفاده از Proxy

یکی دیگه از کارهایی که میشه با Proxy انجام داد، جلوگیری از تغییر یه آبجکت در جاوااسکریپته.یعنی Proxy بهمون اجازه میده Immutable Data بسازیم. درسته که توی نسخه ES6، عبارت Const معرفی شد، اما این عبارت مثل زبان های دیگه که در اونها یه ثابت (Constant) میسازه، عمل نمیکنه. به عبارتی دیگه نباید انتظار داشته باشیم که Const برامون ثابت بسازه. برای اطلاعات بیشتر میتونید به لینک های زیر سر بزنین:

در مورد Object.freeze هم ایراد وارده جون فقط یه لول از آبجکت رو فریز میکنه.

let testObj = Object.freeze({id: 1, name: {first: 'Farzad'}})

testObj.id = 2 // Would'nt affect the testObj.id
testObj.name.first = 'Ali' // Changed testObj

وقتی یه آبجکت Freeze میشه یعنی دیگه نمیشه آیتم های درونش رو عوض کرد. در صورتی که توی اون آبجکت، آبجکت های دیگه ای هم وجود داشته باشن و از تغییر اونا هم جلوگیری کنیم، اسمش میشه Deep Freeze.

بریم سراغ کد و یه هندلر بنویسیم. بیایم اسم هندلرمون رو بزاریم rejector، چون تغییراتی که باید روی آبجکتمون رخ بدن رو reject میکنه.

// Create a freeze factory
const freezeObjectFactory = (obj) => {

  // Our handler that rejects any change to the object and any nested objects inside it
  const rejector = {
    get(obj, prop) {

      // If dealing with nested object, nest the proxy untill it reaches the direct property of it's parent proxy
      if (typeof obj[prop] === 'object' && obj[prop] !== null) return new Proxy(obj[prop], rejector)
      
      // If prop is directly accessible, just do the default operation
      else return obj[prop]

    },

    set(obj, prop, val, rec) {

      console.warn(`Can not set prop ${prop} on freezed object`)
      
      // Return the proxy itself.
      // Note that we could return false, since returning false will create a TypeError, the latter code would always have to be inside a try-catch block which is immpossible and not flexbile.
      return rec
      
    }
  }
}

// Now let's use it and put it into test
// Start our object with default values
let testObj = freezeObjectFactory({name: 'Farzad', parent: {father: {name: 'Ali'}}})

// try to change values
testObj.name = 'John' // Can not set prop name on freezed object

testObj.parent.father.name = 'Mark' // Can not set prop name on freezed object

testObj.id = 1 // Can not set prop id on freezed object

console.log(testObj) // Still the same object as defined above

خب برای اینکه آبجکت هامون رو Freeze کنیم، اولش یه فکتوری ساختم که اگر آبجکتمون رو بهش پاس بدیم، پراکسی شده آبجکتمون رو بهمون میده که دیگه قابل تغییر نیست.

متد Get هندلرمون چک میکنه، اگر آیتمی که داریم میخونیمش مستقیم توی خود آبجکت قرار داره همونو بهمون میده. در غیر این صورت به این معنیه که توی یه آبجکت درونی قرار گرفته. ما تنها آبجکت بیرونی رو از پراکسی گذروندیم، یعنی اگر بخوایم آیتم های درونی مثل testObj.parent.father.name رو ست کنیم دیگه متد Set فراخوانی نمیشه تا جلوی تغییر مقادیر گرفته بشه. پس تا وقتی که با آیتم های تودرتو طرفیم باید اونارو به صورت داخلی از پراکسی بگذرونیم. این توضیح، خط اول متد get رو کامل روشن میکنه.

در متد Set هم باید دقت کنیم پارامتر ۴ام این متد همون آبجکتی هست که در ابتدا به پروکسی پاس داده شد یعنی

{name: 'Farzad', parent: {father: {name: 'Ali'}}}

پس میتونیم همون رو ریترن کنیم. دقت کنید توی کامنت توضیح داده شده که اگر return false کنیم اونوقت پراکسی TypeError ریترن میکنه و این یعنی اگر Exception اش گرفته نشه برنامه خط های بعدی رو ادامه نمیده و خارج میشه. این به این معنیه که خط های بعدی باید کلا در یه بلاک try-catch قرار بگیره که غیر ممکن و مسخره اس. پس به جای return false ما خود آبجکت رو ریترن میکنیم.

نمونه کد بالا روی توی کدپن گذاشتم تا راحت تر تستش کنین:

See the Pen Deep Freeze objects using ES6 Proxy by Farzad YZ (@farskid) on CodePen.

ساخت آبجکت های دفاعی با استفاده از Proxy

آبجکت دفاعی یه تعریف خودساخته اس از آبجکتی که فقط آیتم جدید و غیر تکراری قبول میکنه. در صورتی که بخواین مقدار موجود در آبجکت رو تغییر بدین جلوتون رو میگیره. اما اگر بخواین یه آیتم غیرتکراری جدید بهش اضافه کنین،‌ کاری بهتون نداره.

مثلا فرض کنید آبجکتمون let myObj = {name: 'Farzad', siblings: 1} هست. اگر بخوایم آیتم siblings رو آپدیت کنیم، جلومون رو میگیره جون siblings وجود داره. اما اگر بخوایم مثلا myObj.age = 25 رو انجام بدیم مشکلی نداره چون age وجود نداره.

چون این شرایط خیلی به شرایط قبلی نزدیکه (Deep Freeze)، همون کد رو ویرایش میکنیم.

// Create a defensive factory
const defensiveObjectFactory = (obj) => {

  // Our handler that rejects any change to the object and any nested objects inside it
  const rejector = {
    get(obj, prop) {

      // If dealing with nested object, nest the proxy untill it reaches the direct property of it's parent proxy
      if (typeof obj[prop] === 'object' && obj[prop] !== null) return new Proxy(obj[prop], rejector)
      
      // If prop is directly accessible, just do the default operation
      else return obj[prop]

    },

    set(obj, prop, val, rec) {

      // This time we traverse the object tree to see whether it contains prop or not
      
      // If prop is not currently present in obj, add it
      if (!(prop in obj)) {
        obj[prop] = val
        return true
      }

      // Else warn about it and prevent assignment
      console.warn(`Can not set prop ${prop} on freezed object`)
      
      // Return the proxy itself.
      // Note that we could return false, since returning false will create a TypeError, the latter code would always have to be inside a try-catch block which is immpossible and not flexbile.
      return rec
      
    }
  }
}

// Now let's use it and put it into test
// Start our object with default values
let testObj = freezeObjectFactory({name: 'Farzad', parent: {father: {name: 'Ali'}}})

// try to change values
testObj.name = 'John' // Can not set prop name on freezed object

testObj.parent.father.name = 'Mark' // Can not set prop name on freezed object

testObj.id = 1 // In this case it could be added cause id is new

console.log(testObj) // not the same object, it has `id` on it

اینبار اول چک میکنیم اگر آیتم درون آبجکت وجود نداشته باشه به آبجکت اضافه میشه مثل testObj.id. اما جلوی تغییر آیتم های از پیش قرار گرفته مثل name رو میگیریم.

نمونه در کدپن:

See the Pen ES6 Proxy: Protect object properties by Farzad YZ (@farskid) on CodePen.

جمع بندی

توی این مطلب با Proxy در جاوااسکریپت آشنا شدیم و فهمیدیم که بهمون اجازه میده عملکردهایی مثل خوندن یه آیتم و اضافه کردن آیتم جدید به آبجکت رو کنترل کنیم. درست مثل کاری که Proxy های شبکه ای برامون انجام میدن. کاربردهایی که از Proxy جذاب بودن رو هم با هم مرور کردیم مثل Deep Freeze کردن یه آبجکت یا Defensive کردن آبجکت ها. همینطور یاد گرفتیم که میتونیم تغییرات روی یه آبجکت رو با Proxy ردیابی کنیم و شبیه سازی ای از Object.observe رو بسازیم.

اگر از خوندن این مطلب لذت بردین میتونین اونو با دیگران به اشتراک بزارید. همینطور میتونین از سایر مطالب بلاگ هم استفاده کنین و یا اگر دوس داشته باشید، کنارمون بنویسید. برای راهنمای انتشار مطلب در پول ریکوئست میتونین از این لینک استفاده کنین.