ایجاد اشیاء دفاعی با ES 6 Proxy
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: شش دقیقه

ممکن است برای شما نیز پیش آمده باشد که به یک خصوصیت از یک شیء که وجود ندارد، ارجاع داده باشید و متوجه علت خطای رخ داده نشده و مدتی را به دنبال علت خطا صرف کرده باشید. بعضی از افراد به همین علت از جاوااسکریپت متنفر هستند و می‌گویند اگر از یک زبان type-safe استفاده می‌کردیم آنگاه در صورتیکه به خصوصیتی ارجاع می‌دادیم که وجود ندارد، نبودن خصوصیت ارجاع داده شده را اعلام می‌کرد. این مشکل وجود داشت تا وقتی که ECMAScript 6 ارائه شد.

ECMAScript 5

در حالیکه ECMAScript 5 قابلیت‌های فوق العاده‌ای را برای کنترل کردن خصوصیات موجود در اشیاء، در اختیار شما قرار می‌دهد، اما هیچ راه کاری را برای خصوصیاتی که موجود نیستند، ندارد. شما می‌توانید برای خواص موجود، از رونویسی (تنظیم writable برابر false) و یا حذف شدن (تنظیم configurable برابر false) جلوگیری کنید. شما می‌توانید از اختصاص خصوصیات جدید به اشیاء با استفاده از ()Object.preventExtensions و یا تنظیم تمام خصوصیات به صورت فقط خواندنی و یا غیرقابل حذف ()Object.freeze جلوگیری کنید.

اگر شما نمی‌خواهید تمام خصوصیات را فقط خواندنی کنید می‌توانید از ()Object.seal استفاده کنید. این‌ها مانع از اضافه کردن خصوصیات و یا حذف کردن خصوصیات موجود می‌شوند. اگر به یک شیء مهر و موم شده (sealed)، زمانی که از strict mode استفاده می‌کنید، یک خصوصیت جدید اضافه کنید باعث ایجاد خطا می‌شود:

"use strict";

var person = {
    name: "Vahid Mohammad Taheri"
};

Object.seal(person);
person.age = 27;    // Error!
این کار باعث اطلاع شما می‌شود که در حال تلاش برای تغییر اینترفیس یک شیء، با استفاده از اضافه کردن یک ویژگی به آن هستید. هنگامیکه سعی در خواندن ویژگی از یک شیء که جزئی از اینترفیس آن نیست، دارید نیز با خطا مواجه می‌شوید.

نجات با Proxyها

پروکسی‌ها، دارای سابقه طولانی و پیچیده ای در ECMAScript 6 است. طرح اولیه آن توسط Firefox و Chrome قبل از تصمیم TC-39 به تغییر پروکسی‌ها، اجرا شده است. این تغییرات، برای بهتر و روان‌تر شدن پروکسی‌ها از طرح اولیه پروکسی‌ها انجام گرفت.

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

این کار از طریق یک سری از روش‌هایی که به مخفی کردن عملیات در ECMAScript مطابقت دارند، انجام می‌شود. به عنوان مثال زمانیکه بر روی یک ویژگی از یک شیء، عمل خواندن انجام می‌شود، عملیات  [[Get]] در موتور جاوااسکریپت انجام می‌گیرد. نحوه‌ی رفتار [[Get]] را نمی‌توان تغییر داد؛ با این حال، با استفاده از پروکسی‌ها می‌توان دامی برای زمان فراخوانی [[Get]]  قرار داد و عملیات خاص مورد نظر خود را اعمال کرد. به مثال زیر توجه کنید:

var proxy = new Proxy({ name: "Vahid" }, {
    get: function(target, property) {
        if (property in target) {
            return target[property];
        } else {
            return 13;
        }
    }
});

console.log(proxy.time);        // 13
console.log(proxy.name);        // "Vahid"
console.log(proxy.title);       // 13
این پروکسی از یک شیء ساخته شده به عنوان هدف (آرگومان اول به ()Proxy) استفاده می‌کند. آرگومان دوم دامی را که می‌خواهید برای این شیء بسازید، تعریف می‌کند. با استفاده از متد get عملیات مربوط به [[Get]] به دام افتاده و تابع تعریف شده‌ی ما اجرا می‌شود (باقی عملیات به صورت عادی اجرا می‌شوند). دامی که برای شیء مورد نظر تعریف کرده‌ایم دو پارامتر دریافت می‌کند (اول شی هدف، دوم ویژگی مورد نظر). با استفاده از کد نوشته شده در آن ابتدا بررسی می‌شود که شیء مورد نظر دارای ویژگی ارسال شده است یا خیر؟ در صورتی که وجود داشته باشد، مقدار آن بازگشت داده می‌شود و در غیر اینصورت به صورت ثابت مقدار 13 برگشت داده می‌شود.

برای ایجاد اشیاء دفاعی لازم است چگونگی رهگیری عملیات [[Get]] را درک کنید و هدف از این کار صدور خطا در زمان دستیابی به ویژگی ای از شیءایی که وجود ندارد است.
function createDefensiveObject(target) {

    return new Proxy(target, {
        get: function(target, property) {
            if (property in target) {
                return target[property];
            } else {
                throw new ReferenceError("Property \"" + property + "\" does not exist.");
            }
        }
    });
}

تابع ()createDefensiveObject  یک شیء را به عنوان هدف می‌پذیرد و یک شیء دفاعی برای آن ایجاد می‌کند. پروکسی یک دام به نام get دارد؛ برای زمانی که عمل خواندن انجام می‌شود. اگر ویژگی خوانده شده در شیء وجود داشت، مقدار آن برگشت داده می‌شود و از سوی دیگر، وقتی ویژگی خوانده شده در شیء وجود نداشته باشد، سبب بروز خطا می‌شود. به مثال زیر توجه کنید:
var person = {
    name: "Vahid"
};

var defensivePerson = createDefensiveObject(person);

console.log(defensivePerson.name);        // "Vahid"
console.log(defensivePerson.age);         // Error!
در اینجا ویژگی name به طور معمول کار خواهد کرد؛ ولی ویژگی age باعث صدور خطا می‌شود.
اشیاء دفاعی باعث می‌شوند تا بر روی ویژگی‌هایی که در شیء وجود دارند، بتوان عمل خواندن را انجام داد و در ویژگی‌هایی که موجود نیستند در هنگام خواندن، باعث صدور پیام خطا می‌شوند. با این حال هنوز هم شما می‌توانید ویژگی‌های جدید را بدون خطا اضافه کنید:
var person = {
    name: "Vahid"
};

var defensivePerson = createDefensiveObject(person);

console.log(defensivePerson.name);        // "Vahid"

defensivePerson.age = 13;
console.log(defensivePerson.age);         // 13
بنابراین اشیاء توانایی خود را برای جهش و تغییر حفظ می‌کنند. در صورتی که شما چیزی را برای تغییر آنها انجام دهید، همیشه می‌توانید ویژگی‌هایی را به اشیاء اضافه کنید ولی عمل خواندن بر روی ویژگی‌های غیرموجود همیشه باعث صدور خطا و بازگشت مقدار undefined می‌شود.
روش‌های تشخیص ویژگی‌های استاندارد هنوز هم به طور معمول و بدون خطا کار می‌کنند.
var person = {
    name: "Vahid"
};

var defensivePerson = createDefensiveObject(person);

console.log("name" in defensivePerson);               // true
console.log(defensivePerson.hasOwnProperty("name"));  // true

console.log("age" in defensivePerson);                // false
console.log(defensivePerson.hasOwnProperty("age"));   // false
شما می‌توانید از اینترفیس یک شیء، زمانیکه دسترسی به یک ویژگی آن وجود ندارد، صورت می‌گیرد، با رد کردن اضافات و صدور پیام‌های خطا، دفاع کنید.
var person = {
    name: "Vahid"
};

Object.preventExtensions(person);

var defensivePerson = createDefensiveObject(person);


defensivePerson.age = 27;                 // Error!
console.log(defensivePerson.age);         // Error!
در این مورد، defensivePerson برای هر دو حالت خواندن و نوشتن ویژگی‌هایی که وجود ندارند، خطا صادر می‌کند.
شاید مفیدترین زمان برای استفاده از اشیاء دفاعی، در هنگام تعریف یک سازنده باشد و شما می‌توانید این کار را به عنوان یک قرارداد در نوشتن اشیاء حفظ کنید.
برای مثال:
function Person(name) {
    this.name = name;

    return createDefensiveObject(this);
}

var person = new Person("Vahid");
console.log(person.age);         // Error!
به وسیله فراخوانی تابع ()createDefensiveObject درون سازنده، می‌توانید اطمینان کامل داشته باشید که همه‌ی نمونه‌های ساخته شده‌ی از شیء Person، دارای حالت دفاعی می‌باشند.