معرفی پراکسی های ES2015

آدی عثمانی
Addy Osmani

پروکسی‌های ES2015 (در کروم 49 و نسخه‌های جدیدتر) جاوا اسکریپت را با یک API واسطه ارائه می‌کنند، که به ما امکان می‌دهد همه عملیات‌ها را روی یک شی هدف به دام بیندازیم یا رهگیری کنیم و نحوه عملکرد این هدف را تغییر دهیم.

پراکسی ها کاربردهای زیادی دارند، از جمله:

  • استراق سمع
  • مجازی سازی اشیا
  • مدیریت منابع
  • پروفایل یا ورود به سیستم برای اشکال زدایی
  • امنیت و کنترل دسترسی
  • قراردادهای استفاده از شی

Proxy API حاوی یک سازنده پروکسی است که یک شی هدف تعیین شده و یک شی کنترل کننده را می گیرد.

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

رفتار یک پروکسی توسط کنترل کننده کنترل می شود، که می تواند رفتار اصلی شی هدف را به چند روش مفید تغییر دهد. کنترل کننده حاوی متدهای اختیاری تله است (به عنوان مثال .get() .set() .apply() ) که زمانی که عملیات مربوطه روی پراکسی انجام می شود فراخوانی می شود.

استراق سمع

بیایید با گرفتن یک شی ساده و اضافه کردن برخی از میان افزارهای رهگیری به آن با استفاده از Proxy API شروع کنیم. به یاد داشته باشید، اولین پارامتر ارسال شده به سازنده، هدف (شئی که پروکسی می شود) و دومین پارامتر کنترل کننده (خود پروکسی) است. اینجاست که می‌توانیم قلاب‌هایی را برای گیرنده‌ها، ستترها یا سایر رفتارهای خود اضافه کنیم.

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

با اجرای کد بالا در کروم 49 موارد زیر را دریافت می کنیم:

get was called for: power  
"Flight"

همانطور که در عمل می بینیم، اجرای درستی از ویژگی get یا مجموعه ویژگی ما بر روی شی پراکسی منجر به فراخوانی سطح متا به تله مربوطه در handler شد. عملیات Handler شامل خواندن ویژگی، تخصیص ویژگی و اعمال تابع است که همه آنها به دام مربوطه ارسال می شوند.

تابع trap در صورت تمایل می تواند یک عملیات را به صورت دلخواه اجرا کند (مثلاً ارسال عملیات به شی مورد نظر). اگر تله ای مشخص نشود، در واقع این همان چیزی است که به طور پیش فرض اتفاق می افتد. به عنوان مثال، در اینجا یک پروکسی ارسال بدون عملیات وجود دارد که این کار را انجام می دهد:

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

ما فقط به پراکسی کردن اشیاء ساده نگاه کردیم، اما به همین راحتی می توانیم یک شی تابع را پراکسی کنیم، جایی که یک تابع هدف ما است. این بار از تله handler.apply() استفاده می کنیم:

// Proxying a function object
function sum(a, b) {
    return a + b;
}

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

شناسایی پروکسی ها

هویت یک پروکسی را می توان با استفاده از عملگرهای برابری جاوا اسکریپت ( == و === ) مشاهده کرد. همانطور که می دانیم، وقتی روی دو شیء اعمال می شود، این عملگرها هویت های شی را مقایسه می کنند. مثال بعدی این رفتار را نشان می دهد. مقایسه دو پراکسی مجزا، با وجود یکسان بودن اهداف اساسی، false را برمی‌گرداند. در روشی مشابه، شی هدف با هر یک از پراکسی های آن متفاوت است:

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

در حالت ایده‌آل، شما نباید بتوانید یک پروکسی را از یک شی غیر پراکسی تشخیص دهید، به طوری که قرار دادن یک پروکسی در محل واقعاً بر نتیجه برنامه شما تأثیر نمی گذارد. این یکی از دلایلی است که Proxy API راهی برای بررسی اینکه آیا یک شی یک پروکسی است یا نه تله‌هایی برای همه عملیات روی اشیاء ارائه نمی‌کند.

موارد استفاده کنید

همانطور که گفته شد، پروکسی ها دارای طیف گسترده ای از موارد استفاده هستند. بسیاری از موارد بالا، مانند کنترل دسترسی و نمایه سازی، تحت پوشش های عمومی قرار می گیرند: پراکسی هایی که اشیاء دیگر را در همان آدرس «فضا» قرار می دهند. مجازی سازی نیز ذکر شد. اشیاء مجازی پروکسی هایی هستند که اشیاء دیگر را بدون نیاز به قرار گرفتن در همان فضای آدرس تقلید می کنند. مثال‌ها شامل اشیاء دور (که اشیاء را در فضاهای دیگر شبیه‌سازی می‌کنند) و آینده‌های شفاف (تقلید کردن نتایجی که هنوز محاسبه نشده‌اند) است.

پروکسی ها به عنوان Handler

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

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

    // The default behavior to store the value
    obj[prop] = value;
    }
};

var person = new Proxy({}, validator);

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

نمونه‌های پیچیده‌تر این الگو ممکن است تمام عملیات‌های مختلفی را که کنترل‌کننده‌های پروکسی می‌توانند رهگیری کنند، در نظر بگیرند. می‌توان پیاده‌سازی را تصور کرد که باید الگوی بررسی دسترسی و ارسال عملیات در هر تله را تکرار کند.

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

پسوند شی

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

function extend(sup,base) {

    var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");

    base.prototype = Object.create(sup.prototype);

    var handler = {
    construct: function(target, args) {
        var obj = Object.create(base.prototype);
        this.apply(target,obj, args);
        return obj;
    },

    apply: function(target, that, args) {
        sup.apply(that,args);
        base.apply(that,args);
    }
    };

    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

کنترل دسترسی

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

استفاده از بازتاب با پراکسی ها

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

زبان‌های تایپ ایستا مانند پایتون یا سی شارپ مدت‌هاست که API بازتابی ارائه می‌دهند، اما جاوا اسکریپت واقعاً نیازی به یک زبان پویا ندارد. می توان استدلال کرد که ES5 از قبل دارای چندین ویژگی بازتابی است، مانند Array.isArray() یا Object.getOwnPropertyDescriptor() که در زبان های دیگر انعکاس محسوب می شوند. ES2015 یک API Reflection را معرفی می‌کند که روش‌های آینده را برای این دسته در خود جای می‌دهد و استدلال آنها را آسان‌تر می‌کند. این امر منطقی است زیرا Object به‌عنوان یک نمونه اولیه پایه است تا یک سطل برای روش‌های بازتاب.

با استفاده از Reflect، می‌توانیم نمونه ابرقهرمانی قبلی خود را برای رهگیری میدانی مناسب در تله‌های دریافت و تنظیم به شرح زیر بهبود دهیم:

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

کدام خروجی ها:

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

مثال دیگر جایی است که ممکن است کسی بخواهد:

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

  • قابلیت "ذخیره" تغییرات را اضافه کنید، اما فقط در صورتی که داده ها واقعاً اصلاح شده باشند (به طور فرضی به دلیل گران بودن عملیات ذخیره سازی).

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

برای مثال‌های بیشتر Reflect API، به Proxies ES6 توسط Tagtree مراجعه کنید.

Polyfilling Object.observe()

اگرچه ما با Object.observe() خداحافظی می کنیم، اکنون می توان آنها را با استفاده از پراکسی های ES2015 polyfill کرد. Simon Blackwell اخیراً یک شیم Object.observe مبتنی بر پروکسی نوشته است که ارزش بررسی را دارد. اریک آرویدسون همچنین در سال 2012 یک نسخه کاملاً با مشخصات را نوشت.

پشتیبانی از مرورگر

پروکسی های ES2015 در کروم 49، اپرا، مایکروسافت اج و فایرفاکس پشتیبانی می شوند. سافاری سیگنال های عمومی متفاوتی نسبت به این ویژگی داشته است، اما ما همچنان خوش بین هستیم. Reflect در کروم، اپرا و فایرفاکس است و برای Microsoft Edge در حال توسعه است.

Google یک polyfill محدود برای Proxy منتشر کرده است. این را می‌توان فقط برای wrapper‌های عمومی استفاده کرد، زیرا فقط می‌تواند ویژگی‌های پراکسی را که در زمان ایجاد یک پروکسی شناخته شده است، انجام دهد.

بیشتر خواندن