ขอแนะนำพร็อกซี ES2015

แอดดี้ ออสมานี
แอดดี ออสมานี

พร็อกซี ES2015 (ใน Chrome 49 ขึ้นไป) จะให้ JavaScript พร้อม Incession API ซึ่งทำให้เราสามารถดักหรือสกัดกั้นการดำเนินการทั้งหมดในออบเจ็กต์เป้าหมายและแก้ไขวิธีการทำงานของเป้าหมายนี้ได้

พร็อกซีมีประโยชน์จำนวนมาก ได้แก่

  • ตัดบอล
  • ระบบเสมือนจริงของออบเจ็กต์
  • การจัดการทรัพยากร
  • การทำโปรไฟล์หรือการบันทึกเพื่อแก้ไขข้อบกพร่อง
  • การรักษาความปลอดภัยและการควบคุมการเข้าถึง
  • สัญญาสำหรับการใช้ออบเจ็กต์

Proxy API มีตัวสร้างพร็อกซีที่จะนำออบเจ็กต์เป้าหมายที่กำหนดและออบเจ็กต์ตัวแฮนเดิล

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

ลักษณะการทำงานของพร็อกซีจะควบคุมโดยแฮนเดิล ซึ่งแก้ไขลักษณะการทำงานเดิมของออบเจ็กต์ target ได้หลายวิธี ตัวแฮนเดิลมีเมธอดกับดักที่เป็นตัวเลือก (เช่น .get(), .set(), .apply()) ที่เรียกใช้เมื่อดำเนินการที่เกี่ยวข้องในพร็อกซี

ตัดบอล

เรามาเริ่มด้วยการนำออบเจ็กต์ทั่วไปและเพิ่มมิดเดิลแวร์สกัดกั้นเข้าไปในออบเจ็กต์โดยใช้ Proxy API โปรดจำไว้ว่าพารามิเตอร์แรกที่ส่งผ่านไปยังตัวสร้างคือเป้าหมาย (ออบเจ็กต์ที่กำลังอยู่ในพร็อกซี) และพารามิเตอร์ที่สองคือเครื่องจัดการ (พร็อกซีเอง) ซึ่งเราสามารถเพิ่มฮุกสำหรับ Getter, setter หรือพฤติกรรมอื่นๆ ของเรา

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);

เมื่อใช้โค้ดข้างต้นใน Chrome 49 เราจะได้รับสิ่งต่อไปนี้

get was called for: power  
"Flight"

ตามที่เราเห็นในทางปฏิบัติ การดำเนินการรับพร็อพเพอร์ตี้หรือตั้งค่าพร็อพเพอร์ตี้ในออบเจ็กต์พร็อกซีอย่างถูกต้อง ส่งผลให้เกิดการเรียกระดับเมตาไปยังกับดักที่เกี่ยวข้องบนตัวแฮนเดิล การทำงานของเครื่องจัดการจะรวมถึงการอ่านพร็อพเพอร์ตี้ การกำหนดพร็อพเพอร์ตี้ และแอปพลิเคชันฟังก์ชัน ซึ่งทั้งหมดนี้จะถูกส่งต่อไปยังกับดักที่เกี่ยวข้อง

หากเลือก ฟังก์ชันกับดักสามารถเรียกใช้การดำเนินการที่กำหนดเองได้ (เช่น ส่งต่อการดำเนินการไปยังออบเจ็กต์เป้าหมาย) ซึ่งจะเป็นสิ่งที่เกิดขึ้นตามค่าเริ่มต้นหากไม่ได้ระบุกับดักไว้ เช่น ต่อไปนี้คือพร็อกซีการส่งต่อที่ไม่มีการดำเนินการซึ่งทำหน้าที่นี้เท่านั้น

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

การระบุพร็อกซี

ข้อมูลระบุตัวตนของพร็อกซีจะสังเกตได้โดยใช้โอเปอเรเตอร์ความเท่าเทียมของ JavaScript (== และ ===) ดังที่เราทราบ เมื่อนำไปใช้กับออบเจ็กต์ 2 ตัว โอเปอเรเตอร์เหล่านี้จะเปรียบเทียบข้อมูลประจำตัวของออบเจ็กต์ ตัวอย่างถัดไปแสดงถึงพฤติกรรมนี้ การเปรียบเทียบพร็อกซีที่แตกต่างกัน 2 รายการจะแสดงผลเป็น "เท็จ" แม้ว่าเป้าหมายเบื้องหลังจะเหมือนกัน ในเส้นเลือดดำที่คล้ายคลึงกัน วัตถุเป้าหมายจะแตกต่างจากพร็อกซีใดๆ ก็ตาม:

// Continuing previous example

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

ตามหลักแล้ว คุณไม่ควรแยกพร็อกซีกับออบเจ็กต์ที่ไม่ใช่พร็อกซี เพื่อให้การใส่พร็อกซีจะไม่ส่งผลต่อผลลัพธ์ของแอปจริงๆ เหตุผลหนึ่งที่ Proxy API ไม่มีวิธีตรวจสอบว่าออบเจ็กต์เป็นพร็อกซีหรือไม่ หรือมีกับดักสำหรับการดำเนินการทั้งหมดในออบเจ็กต์

Use Case

ตามที่กล่าวไปแล้ว พร็อกซีมีกรณีการใช้งานที่หลากหลาย การดำเนินการข้างต้นหลายรายการ เช่น การควบคุมการเข้าถึงและการทำโปรไฟล์อยู่ภายใต้ Wrapper ทั่วไป ซึ่งเป็นพร็อกซีที่รวมออบเจ็กต์อื่นๆ ใน "space" ที่อยู่เดียวกัน และยังมีการพูดถึงระบบเสมือนจริงอีกด้วย วัตถุเสมือนจริงเป็นพร็อกซีที่จำลองวัตถุอื่นๆ โดยวัตถุนั้นไม่จำเป็นต้องอยู่ในพื้นที่ที่อยู่เดียวกัน ตัวอย่างเช่น ออบเจ็กต์ระยะไกล (ที่จำลองออบเจ็กต์ในพื้นที่ทำงานอื่นๆ) และฟิวเจอร์สแบบโปร่งใส (จำลองผลลัพธ์ที่ยังไม่ได้คำนวณ)

พร็อกซีในฐานะเครื่องจัดการ

กรณีการใช้งานทั่วไปสำหรับตัวแฮนเดิลพร็อกซีคือการตรวจสอบความถูกต้องหรือการควบคุมการเข้าถึงก่อนดำเนินการกับออบเจ็กต์ที่รวมไว้ ระบบจะส่งต่อการดำเนินการต่อเมื่อการตรวจสอบสำเร็จเท่านั้น ตัวอย่างการตรวจสอบด้านล่างนี้

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 เป็นวัตถุในตัวใหม่ซึ่งให้เมธอดสำหรับการดำเนินการ JavaScript ที่สกัดกั้นได้ ซึ่งมีประโยชน์มากสำหรับการทำงานกับพร็อกซี ในความเป็นจริง เมธอด Reflect จะเหมือนกับเมธอดของเครื่องจัดการพร็อกซี

ภาษาที่พิมพ์แบบคงที่ เช่น Python หรือ C# มี API การสะท้อนมานานแล้ว แต่ JavaScript ไม่จำเป็นนักที่แปลว่าเป็นภาษาแบบไดนามิก บางคนอาจโต้แย้งว่า ES5 มีฟีเจอร์การทบทวนอยู่บ้างแล้ว เช่น Array.isArray() หรือ Object.getOwnPropertyDescriptor() ซึ่งถือว่าเป็นการสะท้อนในภาษาอื่น ES2015 เปิดตัว Reflection API ซึ่งจะเก็บเมธอดในอนาคตสำหรับหมวดหมู่นี้ เพื่อช่วยให้เข้าใจได้ง่ายขึ้น ซึ่งเหมาะสมแล้ว เพราะ Object มีจุดประสงค์เพื่อเป็นต้นแบบพื้นฐานมากกว่าที่จะเป็นที่เก็บข้อมูลสำหรับวิธีการสะท้อนกลับ

การใช้ Reflect ช่วยให้เราปรับปรุงตัวอย่าง Superhero ในก่อนหน้านี้ให้มีการสกัดกั้นสนามที่ถูกต้องในการเข้าและกับดัก ดังนี้

// 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 เพิ่มเติมได้ที่ ES6 Proxies โดย Tagtree

Polyfilling Object.observe()

แม้ว่าเราจะต้องบอกลากับ Object.observe() แต่ปัจจุบันคุณสามารถใช้พร็อกซี ES2015 ได้ Simon Blackwell ได้เขียน shim ด้วย Object.observe() ที่อิงตามพร็อกซี ซึ่งคุ้มค่าที่จะดูข้อมูล Erik Arvidsson ยังเขียนข้อกำหนดที่ค่อนข้างสมบูรณ์ไว้ย้อนไปเมื่อปี 2012 ด้วย

การสนับสนุนเบราว์เซอร์

พร็อกซี ES2015 ใช้ได้ใน Chrome 49, Opera, Microsoft Edge และ Firefox Safari ได้มีสัญญาณจากสาธารณะหลายอย่างที่พูดถึงฟีเจอร์นี้ แต่เรายังคงมองโลกในแง่ดี Reflect อยู่ใน Chrome, Opera และ Firefox และอยู่ระหว่างการพัฒนาสำหรับ Microsoft Edge

Google ได้เปิดตัว Polyfill ที่จำกัดสำหรับพร็อกซี ซึ่งใช้ได้เฉพาะกับ Wrapper ทั่วไปเท่านั้น เนื่องจากจะสามารถใช้ได้เฉพาะพร็อพเพอร์ตี้พร็อกซีที่รู้จักเมื่อมีการสร้างพร็อกซี

อ่านเพิ่มเติม