พร็อกซี ES2015 (ใน Chrome 49 ขึ้นไป) มี API สื่อกลางสำหรับ JavaScript ซึ่งช่วยให้เราดักหรือขัดขวางการดำเนินการทั้งหมดในออบเจ็กต์เป้าหมายและแก้ไขวิธีการทำงานของเป้าหมายนี้ได้
พร็อกซีมีการใช้งานหลายอย่าง เช่น
- ตัดบอล
- การจำลองเสมือนออบเจ็กต์
- การจัดการทรัพยากร
- โปรไฟล์หรือการบันทึกเพื่อแก้ไขข้อบกพร่อง
- การรักษาความปลอดภัยและการควบคุมการเข้าถึง
- สัญญาสําหรับการใช้วัตถุ
Proxy API มีตัวสร้างพร็อกซีที่ใช้ออบเจ็กต์เป้าหมายที่กําหนดไว้และออบเจ็กต์แฮนเดิล
var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);
ตัวแฮนเดิลจะควบคุมลักษณะการทํางานของพร็อกซี ซึ่งสามารถแก้ไขลักษณะการทํางานเดิมของออบเจ็กต์เป้าหมายได้หลายวิธี แฮนเดิลมีเมธอดกับดักที่เลือกได้ (เช่น .get()
, .set()
, .apply()
) ซึ่งจะเรียกใช้เมื่อดำเนินการที่เกี่ยวข้องในพร็อกซี
ตัดบอล
มาเริ่มกันด้วยการใช้ออบเจ็กต์ธรรมดาและเพิ่มมิดเดิลแวร์การขัดจังหวะเข้าไปโดยใช้ Proxy API โปรดทราบว่าพารามิเตอร์แรกที่ส่งไปยังคอนสตรคเตอร์คือเป้าหมาย (ออบเจ็กต์ที่ใช้พร็อกซี) และพารามิเตอร์ที่ 2 คือตัวแฮนเดิล (พร็อกซีเอง) ในส่วนนี้เราจะเพิ่มฮุกสําหรับ 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
การระบุพร็อกซี
คุณสามารถดูตัวตนของพร็อกซีได้โดยใช้โอเปอเรเตอร์ Equality ของ JavaScript (==
และ ===
) ดังที่เราทราบกันดีว่าเมื่อใช้กับออบเจ็กต์ 2 รายการ โอเปอเรเตอร์เหล่านี้จะเปรียบเทียบตัวตนของออบเจ็กต์ ตัวอย่างถัดไปแสดงลักษณะการทํางานนี้ การเปรียบเทียบพร็อกซีที่แตกต่างกัน 2 รายการจะแสดงผลเป็นเท็จ แม้ว่าเป้าหมายพื้นฐานจะเหมือนกันก็ตาม ในทํานองเดียวกัน ออบเจ็กต์เป้าหมายจะแตกต่างจากพร็อกซีทั้งหมดของออบเจ็กต์นั้น
// Continuing previous example
var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false
ตามหลักการแล้ว คุณไม่ควรแยกแยะพร็อกซีออกจากออบเจ็กต์ที่ไม่ใช่พร็อกซีเพื่อให้การใช้พร็อกซีไม่ส่งผลต่อผลลัพธ์ของแอปจริงๆ นี่เป็นเหตุผลหนึ่งที่ทำให้ Proxy API ไม่มีวิธีตรวจสอบว่าออบเจ็กต์เป็นพร็อกซีหรือไม่ และไม่มีกับดักสำหรับการดำเนินการทั้งหมดกับออบเจ็กต์
กรณีการใช้งาน
ดังที่ได้กล่าวไว้ข้างต้น พารามิเตอร์พร็อกซีมีกรณีการใช้งานที่หลากหลาย รายการข้างต้นจำนวนมาก เช่น การควบคุมการเข้าถึงและการสร้างโปรไฟล์ อยู่ภายใต้ตัวห่อทั่วไป: พร็อกซีที่รวมออบเจ็กต์อื่นๆ ไว้ใน "พื้นที่" ที่อยู่เดียวกัน และพูดถึงการจําลองด้วย ออบเจ็กต์เสมือนคือพร็อกซีที่จำลองออบเจ็กต์อื่นๆ โดยที่ออบเจ็กต์เหล่านั้นไม่จำเป็นต้องอยู่ในสเปซที่อยู่เดียวกัน ตัวอย่างเช่น ออบเจ็กต์ระยะไกล (จำลองออบเจ็กต์ในพื้นที่อื่น) และอนาคตแบบโปร่งใส (จำลองผลลัพธ์ที่ยังไม่ได้คํานวณ)
พร็อกซีเป็นตัวแฮนเดิล
กรณีการใช้งานที่พบได้ทั่วไปสำหรับตัวแฮนเดิลพร็อกซีคือการดำเนินการตรวจสอบความถูกต้องหรือการตรวจสอบการควบคุมการเข้าถึงก่อนดำเนินการกับออบเจ็กต์ที่รวมไว้ ระบบจะส่งต่อการดำเนินการต่อเมื่อการตรวจสอบเสร็จสมบูรณ์เท่านั้น ตัวอย่างการตรวจสอบด้านล่างแสดงถึงกรณีนี้
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
การควบคุมการเข้าถึง
การควบคุมการเข้าถึงเป็นอีก Use Case ที่เหมาะกับพร็อกซี แทนที่จะส่งออบเจ็กต์เป้าหมายไปยังโค้ดที่ไม่น่าเชื่อถือ ผู้ใช้อาจส่งพร็อกซีที่รวมอยู่ในชั้นป้องกันได้ เมื่อแอปพิจารณาว่าโค้ดที่ไม่น่าเชื่อถือทำงานบางอย่างเสร็จแล้ว ก็จะเพิกถอนการอ้างอิงซึ่งจะแยกพร็อกซีออกจากเป้าหมาย เมมเบรนจะขยายการแยกนี้แบบซ้ำไปยังออบเจ็กต์ทั้งหมดที่เข้าถึงได้จากเป้าหมายเดิมที่กำหนดไว้
การใช้การสะท้อนกับพร็อกซี
Reflect ออบเจ็กต์ในตัวใหม่ที่มีเมธอดสําหรับการดำเนินการ JavaScript ที่ขัดจังหวะได้ ซึ่งมีประโยชน์มากเมื่อต้องทํางานกับพร็อกซี อันที่จริงแล้ว เมธอดของ Reflect จะเหมือนกับเมธอดของ proxy handler
ภาษาที่มีการกำหนดค่าแบบคงที่ เช่น Python หรือ C# มี API การแสดงผลมาอย่างยาวนานแล้ว แต่ JavaScript ไม่จำเป็นต้องมีเนื่องจากเป็นภาษาแบบไดนามิก บางคนอาจโต้แย้งว่า ES5 มีฟีเจอร์การสะท้อนกลับอยู่หลายรายการแล้ว เช่น Array.isArray()
หรือ Object.getOwnPropertyDescriptor()
ซึ่งจะถือว่าเป็นการสะท้อนกลับในภาษาอื่นๆ ES2015 เปิดตัว Reflection API ซึ่งจะเป็นพื้นที่เก็บเมธอดในอนาคตสำหรับหมวดหมู่นี้ ซึ่งจะทำให้การหาเหตุผลเกี่ยวกับเมธอดเหล่านี้ง่ายขึ้น ซึ่งก็สมเหตุสมผลเนื่องจากออบเจ็กต์มีไว้เพื่อเป็นโปรโตไทป์พื้นฐาน ไม่ใช่ที่เก็บข้อมูลสำหรับเมธอดการสะท้อน
เมื่อใช้ Reflect เราสามารถปรับปรุงตัวอย่าง Superhero ก่อนหน้านี้เพื่อรับมือกับการดักรับฟิลด์ที่เหมาะสมในกับดัก get และ set ดังนี้
// 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
การทดแทน Object.observe()
แม้ว่าเราจะบอกลา Object.observe()
แล้ว แต่ตอนนี้คุณก็ใช้ polyfill โดยใช้ ES2015 Proxies ได้ Simon Blackwell เขียน shim Object.observe() ที่ใช้ Proxy ซึ่งเพิ่งเผยแพร่ไปและควรลองดู นอกจากนี้ Erik Arvidsson ยังได้เขียนเวอร์ชันที่สมบูรณ์ตามข้อกำหนดไว้เมื่อปี 2012
การสนับสนุนเบราว์เซอร์
พร็อกซี ES2015 ใช้งานได้ใน Chrome 49, Opera, Microsoft Edge และ Firefox Safari มีสัญญาณสาธารณะที่หลากหลายเกี่ยวกับฟีเจอร์นี้ แต่เรายังคงมองโลกในแง่ดี โดย Reflect มีให้บริการใน Chrome, Opera และ Firefox และกำลังอยู่ระหว่างการพัฒนาสำหรับ Microsoft Edge
Google ได้เปิดตัว Polyfill แบบจํากัดสําหรับ Proxy ใช้ได้กับตัวห่อทั่วไปเท่านั้น เนื่องจากจะทำหน้าที่เป็นพร็อกซีสำหรับพร็อพเพอร์ตี้ที่ทราบ ณ เวลาที่สร้างพร็อกซีเท่านั้น
อ่านเพิ่มเติม
- ES6 อย่างละเอียด: พร็อกซีและ Reflect
- MDN: ES6 Proxies
- พร็อกซี ES6 และ Reflect ใน TagTree
- MDN: The Reflect Object
- การสะท้อนกลับของ ES6 อย่างละเอียด
- พร็อกซี: หลักการทํางานสําหรับ API สื่อกลางแบบออบเจ็กต์ออเรียนเต็ดที่มีประสิทธิภาพ
- 2ality: Metaprogramming with ES6
- เมตาโปรแกรมมิงใน ES6 โดยใช้ Reflect
- ES6 everyday Reflect