Proxies
프록시(Proxy)를 사용하면 호스트 객체에 다양한 기능을 추가하여 객체를 생성할 수 있습니다. interception, 객체 추상화, 로깅/수집, 값 검증 등에 사용될 수 있습니다. Proxy 객체는 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 새로운 행동을 정의할 때 사용합니다.
구문
- new Proxy(target, handler);
- handler : trap들을 가지고 있는 Placeholder 객체.
- traps : 프로퍼티에 접근할 수 있는 메소드. 운영체제에서 trap 이라는 컨셉과 유사하다.
- target : proxy가 가상화하는 실제 객체. 이것은 proxy를 위한 backend 저장소로 사용된다
get
- Syntax
var p = new Proxy(target, {
get: function(target, property, receiver) {
}
});
- 프로퍼티 이름이 객체에 없을때, 기본값을 숫자 37로 리턴받는 간단한 예제이다. 이것은 get handler 를 사용하였다.
var handler = {
get: function(target, name){
return name in target?
target[name] :
37;
}
};
var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
- No-op forwarding proxy 이 예제에서는, native JavaScript를 사용하겠다. proxy는 적용된 모든 동작으로 보낼 것이다.
var target = {};
var p = new Proxy(target, {});
p.a = 37; // target으로 동작이 전달
console.log(target.a); // 37. 동작이 제대로 전달됨
- Finding an array item object by its property (property로 배열의 객체를 찾기) proxy 는 유용한 특성을 가진 배열로 확장할 것이다. Object.defineProperties를 사용하지 않고, 유연하게 property들을 유연하게 “정의”할 수 있다. 이 예제는 테이블의 cell을 이용해서 row(열)을 찾는데 적용할 수 있다. 이 경우, target은 table.rows가 될 것이다.
let products = new Proxy([
{ name: 'Firefox', type: 'browser' },
{ name: 'SeaMonkey', type: 'browser' },
{ name: 'Thunderbird', type: 'mailer' }
],
{
get: function(obj, prop) {
// The default behavior to return the value; prop is usually an integer
if (prop in obj) {
return obj[prop];
}
// Get the number of products; an alias of products.length
if (prop === 'number') {
return obj.length;
}
let result, types = {};
for (let product of obj) {
if (product.name === prop) {
result = product;
}
if (types[product.type]) {
types[product.type].push(product);
} else {
types[product.type] = [product];
}
}
// Get a product by name
if (result) {
return result;
}
// Get products by type
if (prop in types) {
return types[prop];
}
// Get product types
if (prop === 'types') {
return Object.keys(types);
}
return undefined;
}
});
console.log(products[0]); // { name: 'Firefox', type: 'browser' }
console.log(products['Firefox']); // { name: 'Firefox', type: 'browser' }
console.log(products['Chrome']); // undefined
console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types); // ['browser', 'mailer']
console.log(products.number); // 3
- The handler.get() method is a trap for getting a property value.
const monster1 = {
secret: 'easily scared',
eyeCount: 4
};
const handler1 = {
get: function(target, prop, receiver) {
if (prop === 'secret') {
return `${target.secret.substr(0, 4)} ... shhhh!`;
} else {
return Reflect.get(...arguments);
}
}
};
const proxy1 = new Proxy(monster1, handler1);
console.log(proxy1.eyeCount);
// expected output: 4
console.log(proxy1.secret);
// expected output: "easi ... shhhh!"
- The following code traps getting a property value.
var p = new Proxy({}, {
get: function(target, property, receiver) {
console.log('called: ' + property);
return 10;
}
});
console.log(p.a); // "called: a"
// 10
- The following code violates an invariant
var obj = {};
Object.defineProperty(obj, 'a', {
configurable: false,
enumerable: false,
value: 10,
writable: false
});
var p = new Proxy(obj, {
get: function(target, property) {
return 20;
}
});
p.a; // TypeError is thrown
set
- Syntax
var p = new Proxy(target, {
set: function(target, property, value, receiver) {
}
});
- Validation (검증) Proxy에서, 객체에 전달된 값을 쉽게 검증할 수 있다. 이 예제는 set handler 를 사용하였다.
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // Throws an exception
person.age = 300; // Throws an exception
- Manipulating DOM nodes (DOM nodes 조작) : 가끔씩, 두 개의 다른 element의 속성이나 클래스 이름을 바꾸고 싶을 것이다. 아래는 set handler 를 사용하였다.
let view = new Proxy({
selected: null
},
{
set: function(obj, prop, newval) {
let oldval = obj[prop];
if (prop === 'selected') {
if (oldval) {
oldval.setAttribute('aria-selected', 'false');
}
if (newval) {
newval.setAttribute('aria-selected', 'true');
}
}
// The default behavior to store the value
obj[prop] = newval;
}
});
let i1 = view.selected = document.getElementById('item-1');
console.log(i1.getAttribute('aria-selected')); // 'true'
let i2 = view.selected = document.getElementById('item-2');
console.log(i1.getAttribute('aria-selected')); // 'false'
console.log(i2.getAttribute('aria-selected')); // 'true'
- Value correction and an extra property (값 정정과 추가적인 property) products 라는 proxy 객체는 전달된 값을 평가하고, 필요할 때 배열로 변환한다. 이 객체는 latestBrowser 라는 추가적인 property를 지원하는데, getter와 setter 모두 지원한다.
let products = new Proxy({
browsers: ['Internet Explorer', 'Netscape']
},
{
get: function(obj, prop) {
// An extra property
if (prop === 'latestBrowser') {
return obj.browsers[obj.browsers.length - 1];
}
// The default behavior to return the value
return obj[prop];
},
set: function(obj, prop, value) {
// An extra property
if (prop === 'latestBrowser') {
obj.browsers.push(value);
return;
}
// Convert the value if it is not an array
if (typeof value === 'string') {
value = [value];
}
// The default behavior to store the value
obj[prop] = value;
}
});
console.log(products.browsers); // ['Internet Explorer', 'Netscape']
products.browsers = 'Firefox'; // pass a string (by mistake)
console.log(products.browsers); // ['Firefox'] <- no problem, the value is an array
products.latestBrowser = 'Chrome';
console.log(products.browsers); // ['Firefox', 'Chrome']
console.log(products.latestBrowser); // 'Chrome'
- The handler.set() method is a trap for setting a property value.
function Monster() {
this.eyeCount = 4;
}
const handler1 = {
set(obj, prop, value) {
if ((prop === 'eyeCount') && ((value % 2) !== 0)) {
console.log('Monsters must have an even number of eyes');
} else {
return Reflect.set(...arguments);
}
}
};
const monster1 = new Monster();
const proxy1 = new Proxy(monster1, handler1);
proxy1.eyeCount = 1;
// expected output: "Monsters must have an even number of eyes"
console.log(proxy1.eyeCount);
// expected output: 4
- The following code traps setting a property value.
var p = new Proxy({}, {
set: function(target, prop, value, receiver) {
target[prop] = value;
console.log('property set: ' + prop + ' = ' + value);
return true;
}
})
console.log('a' in p); // false
p.a = 10; // "property set: a = 10"
console.log('a' in p); // true
console.log(p.a); // 10
apply
- Syntax
var p = new Proxy(target, {
apply: function(target, thisArg, argumentsList) {
}
});
- Extending constructor (생성자 확장) : function proxy는 쉽게 새로운 생성자와 함께 생성자를 확장할 수 있다
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 Person = function(name){
this.name = name;
};
var Boy = extend(Person, function(name, age) {
this.age = age;
});
Boy.prototype.sex = "M";
var Peter = new Boy("Peter", 13);
console.log(Peter.sex); // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age); // 13
- The handler.apply() method is a trap for a function call.
function sum(a, b) {
return a + b;
}
const handler = {
apply: function(target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`);
// expected output: "Calculate sum: 1,2"
return target(argumentsList[0], argumentsList[1]) * 10;
}
};
const proxy1 = new Proxy(sum, handler);
console.log(sum(1, 2));
// expected output: 3
console.log(proxy1(1, 2));
// expected output: 30
- The following code traps a function call.
var p = new Proxy(function() {}, {
apply: function(target, thisArg, argumentsList) {
console.log('called: ' + argumentsList.join(', '));
return argumentsList[0] + argumentsList[1] + argumentsList[2];
}
});
console.log(p(1, 2, 3)); // "called: 1, 2, 3"
// 6
construct
- Syntax
var p = new Proxy(target, {
construct: function(target, argumentsList, newTarget) {
}
});
- The handler.construct() method is a trap for the new operator. In order for the new operation to be valid on the resulting Proxy object, the target used to initialize the proxy must itself have a [[Construct]] internal method (i.e. new target must be valid).
function monster1(disposition) {
this.disposition = disposition;
}
const handler1 = {
construct(target, args) {
console.log('monster1 constructor called');
// expected output: "monster1 constructor called"
return new target(...args);
}
};
const proxy1 = new Proxy(monster1, handler1);
console.log(new proxy1('fierce').disposition);
// expected output: "fierce"
- The following code traps the new operator.
var p = new Proxy(function() {}, {
construct: function(target, argumentsList, newTarget) {
console.log('called: ' + argumentsList.join(', '));
return { value: argumentsList[0] * 10 };
}
});
console.log(new p(1).value); // "called: 1"
// 10
var p = new Proxy(function() {}, {
construct: function(target, argumentsList, newTarget) {
return 1;
}
});
new p(); // TypeError is thrown
- The following code improperly initializes the proxy. The target in Proxy initialization must itself be a valid constructor for the new operator.
var p = new Proxy({}, {
construct: function(target, argumentsList, newTarget) {
return {};
}
});
new p(); // TypeError is thrown, "p" is not a constructor
defineProperty
- Syntax
var p = new Proxy(target, {
defineProperty: function(target, property, descriptor) {
}
});
- The handler.defineProperty() method is a trap for Object.defineProperty().
const handler1 = {
defineProperty(target, key, descriptor) {
invariant(key, 'define');
return true;
}
};
function invariant(key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const monster1 = {};
const proxy1 = new Proxy(monster1, handler1);
console.log(proxy1._secret = 'easily scared');
// expected output: Error: Invalid attempt to define private "_secret" property
- The following code traps Object.defineProperty().
var p = new Proxy({}, {
defineProperty: function(target, prop, descriptor) {
console.log('called: ' + prop);
return true;
}
});
var desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, 'a', desc); // "called: a"
- When calling Object.defineProperty() or Reflect.defineProperty(), the descriptor passed to defineProperty trap has one restriction - only following properties are usable, nonstandard properties will be ignored:
- enumerable
- configurable
- writable
- value
- get
- set
var p = new Proxy({}, {
defineProperty(target, prop, descriptor) {
console.log(descriptor);
return Reflect.defineProperty(target, prop, descriptor);
}
});
Object.defineProperty(p, 'name', {
value: 'proxy',
type: 'custom'
}); // { value: 'proxy' }
deleteProperty
- Syntax
var p = new Proxy(target, {
deleteProperty: function(target, property) {
}
});
- The handler.deleteProperty() method is a trap for the delete operator.
const monster1 = {
texture: 'scaly'
};
const handler1 = {
deleteProperty(target, prop) {
if (prop in target) {
delete target[prop];
console.log(`property removed: ${prop}`);
// expected output: "property removed: texture"
}
}
};
console.log(monster1.texture);
// expected output: "scaly"
const proxy1 = new Proxy(monster1, handler1);
delete proxy1.texture;
console.log(monster1.texture);
// expected output: undefined
- The following code traps the delete operator.
var p = new Proxy({}, {
deleteProperty: function(target, prop) {
if (prop in target){
delete target[prop]
console.log('property removed: ' + prop)
return true
}
else {
console.log('property not found: ' + prop)
return false
}
}
})
var result
p.a = 10
console.log('a' in p) // true
result = delete p.a // "property removed: a"
console.log(result) // true
console.log('a' in p) // false
result = delete p.a // "property not found: a"
console.log(result) // false
enumerate
- Syntax
var p = new Proxy(target, {
enumerate(target) {
}
});
- The following code traps for…in statements.
var p = new Proxy({}, {
enumerate(target) {
console.log('called');
return ['a', 'b', 'c'][Symbol.iterator]();
}
});
for (var x in p) { // "called"
console.log(x); // "a"
} // "b"
// "c"
- The following code violates the invariant.
var p = new Proxy({}, {
enumerate(target) {
return 1;
}
});
for (var x in p) {} // TypeError is thrown
getOwnPropertyDescriptor
- Syntax
var p = new Proxy(target, {
getOwnPropertyDescriptor: function(target, prop) {
}
});
const monster1 = {
eyeCount: 4
};
const handler1 = {
getOwnPropertyDescriptor(target, prop) {
console.log(`called: ${prop}`);
// expected output: "called: eyeCount"
return { configurable: true, enumerable: true, value: 5 };
}
};
const proxy1 = new Proxy(monster1, handler1);
console.log(Object.getOwnPropertyDescriptor(proxy1, 'eyeCount').value);
// expected output: 5
- The following code traps Object.getOwnPropertyDescriptor().
var p = new Proxy({ a: 20}, {
getOwnPropertyDescriptor: function(target, prop) {
console.log('called: ' + prop);
return { configurable: true, enumerable: true, value: 10 };
}
});
console.log(Object.getOwnPropertyDescriptor(p, 'a').value); // "called: a"
// 10
- The following code violates an invariant.
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
getOwnPropertyDescriptor: function(target, prop) {
return undefined;
}
});
Object.getOwnPropertyDescriptor(p, 'a'); // TypeError is thrown
getPrototypeOf
- Syntax
var p = new Proxy(obj, {
getPrototypeOf(target) {
...
}
});
const monster1 = {
eyeCount: 4
};
const monsterPrototype = {
eyeCount : 2
};
const handler = {
getPrototypeOf(target) {
return monsterPrototype;
}
};
const proxy1 = new Proxy(monster1, handler);
console.log(Object.getPrototypeOf(proxy1) === monsterPrototype);
// expected output: true
console.log(Object.getPrototypeOf(proxy1).eyeCount);
// expected output: 2
var obj = {};
var proto = {};
var handler = {
getPrototypeOf(target) {
console.log(target === obj); // true
console.log(this === handler); // true
return proto;
}
};
var p = new Proxy(obj, handler);
console.log(Object.getPrototypeOf(p) === proto); // true
- Five ways to trigger the getPrototypeOf trap
var obj = {};
var p = new Proxy(obj, {
getPrototypeOf(target) {
return Array.prototype;
}
});
console.log(
Object.getPrototypeOf(p) === Array.prototype, // true
Reflect.getPrototypeOf(p) === Array.prototype, // true
p.__proto__ === Array.prototype, // true
Array.prototype.isPrototypeOf(p), // true
p instanceof Array // true
);
- Two kinds of exceptions
var obj = {};
var p = new Proxy(obj, {
getPrototypeOf(target) {
return 'foo';
}
});
Object.getPrototypeOf(p); // TypeError: "foo" is not an object or null
var obj = Object.preventExtensions({});
var p = new Proxy(obj, {
getPrototypeOf(target) {
return {};
}
});
Object.getPrototypeOf(p); // TypeError: expected same prototype value
has
- Syntax
var p = new Proxy(target, {
has: function(target, prop) {
}
});
const handler1 = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
const monster1 = {
_secret: 'easily scared',
eyeCount: 4
};
const proxy1 = new Proxy(monster1, handler1);
console.log('eyeCount' in proxy1);
// expected output: true
console.log('_secret' in proxy1);
// expected output: false
console.log('_secret' in monster1);
// expected output: true
- The following code traps the in operator.
var p = new Proxy({}, {
has: function(target, prop) {
console.log('called: ' + prop);
return true;
}
});
console.log('a' in p); // "called: a"
// true
- The following code violates an invariant.
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p; // TypeError is thrown
isExtensible
- Syntax
var p = new Proxy(target, {
isExtensible: function(target) {
}
});
const monster1 = {
canEvolve: true
};
const handler1 = {
isExtensible(target) {
return Reflect.isExtensible(target);
},
preventExtensions(target) {
target.canEvolve = false;
return Reflect.preventExtensions(target);
}
};
const proxy1 = new Proxy(monster1, handler1);
console.log(Object.isExtensible(proxy1));
// expected output: true
console.log(monster1.canEvolve);
// expected output: true
Object.preventExtensions(proxy1);
console.log(Object.isExtensible(proxy1));
// expected output: false
console.log(monster1.canEvolve);
// expected output: false
- The following code traps Object.isExtensible().
var p = new Proxy({}, {
isExtensible: function(target) {
console.log('called');
return true;
}
});
console.log(Object.isExtensible(p)); // "called"
// true
- The following code violates the invariant.
var p = new Proxy({}, {
isExtensible: function(target) {
return false;
}
});
Object.isExtensible(p); // TypeError is thrown
ownKeys
- Syntax
var p = new Proxy(target, {
ownKeys: function(target) {
}
});
const monster1 = {
_age: 111,
[Symbol('secret')]: 'I am scared!',
eyeCount: 4
}
const handler1 = {
ownKeys (target) {
return Reflect.ownKeys(target)
}
}
const proxy1 = new Proxy(monster1, handler1);
for (let key of Object.keys(proxy1)) {
console.log(key);
// expected output: "_age"
// expected output: "eyeCount"
}
- The following code traps Object.getOwnPropertyNames().
var p = new Proxy({}, {
ownKeys: function(target) {
console.log('called');
return ['a', 'b', 'c'];
}
});
console.log(Object.getOwnPropertyNames(p)); // "called"
// [ 'a', 'b', 'c' ]
- The following code violates an invariant.
var obj = {};
Object.defineProperty(obj, 'a', {
configurable: false,
enumerable: true,
value: 10 }
);
var p = new Proxy(obj, {
ownKeys: function(target) {
return [123, 12.5, true, false, undefined, null, {}, []];
}
});
console.log(Object.getOwnPropertyNames(p));
// TypeError: proxy [[OwnPropertyKeys]] must return an array
// with only string and symbol elements
preventExtensions
- Syntax
var p = new Proxy(target, {
preventExtensions: function(target) {
}
});
const monster1 = {
canEvolve: true
};
const handler1 = {
preventExtensions(target) {
target.canEvolve = false;
Object.preventExtensions(target);
return true;
}
};
const proxy1 = new Proxy(monster1, handler1);
console.log(monster1.canEvolve);
// expected output: true
Object.preventExtensions(proxy1);
console.log(monster1.canEvolve);
// expected output: false
- The following code traps Object.preventExtensions().
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
console.log(Object.preventExtensions(p)); // "called"
// false
- The following code violates the invariant.
var p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p); // TypeError is thrown
setPrototypeOf
- Syntax
var p = new Proxy(target, {
setPrototypeOf: function(target, prototype) {
}
});
const handler1 = {
setPrototypeOf(monster1, monsterProto) {
monster1.geneticallyModified = true;
return false;
}
};
const monsterProto = {};
const monster1 = {
geneticallyModified : false
};
const proxy1 = new Proxy(monster1, handler1);
// Object.setPrototypeOf(proxy1, monsterProto); // throws a TypeError
console.log(Reflect.setPrototypeOf(proxy1, monsterProto));
// expected output: false
console.log(monster1.geneticallyModified);
// expected output: true
var handlerReturnsFalse = {
setPrototypeOf(target, newProto) {
return false;
}
};
var newProto = {}, target = {};
var p1 = new Proxy(target, handlerReturnsFalse);
Object.setPrototypeOf(p1, newProto); // throws a TypeError
Reflect.setPrototypeOf(p1, newProto); // returns false
var handlerThrows = {
setPrototypeOf(target, newProto) {
throw new Error('custom error');
}
};
var newProto = {}, target = {};
var p2 = new Proxy(target, handlerThrows);
Object.setPrototypeOf(p2, newProto); // throws new Error("custom error")
Reflect.setPrototypeOf(p2, newProto); // throws new Error("custom error")
종합
- A complete traps list example (완벽한 traps 리스트 예제) 이제 완벽한 traps 리스트를 생성하기 위해서, non native 객체를 프록시화 할 것이다. 이것은 특히, 다음과 같은 동작에 적합하다 : the “little framework” published on the document.cookie page 에 의해 생성된 docCookies 는 글로벌 객체
/*
var docCookies = ... get the "docCookies" object here:
https://developer.mozilla.org/en-US/docs/DOM/document.cookie#A_little_framework.3A_a_complete_cookies_reader.2Fwriter_with_full_unicode_support
*/
var docCookies = new Proxy(docCookies, {
get: function (oTarget, sKey) {
return oTarget[sKey] || oTarget.getItem(sKey) || undefined;
},
set: function (oTarget, sKey, vValue) {
if (sKey in oTarget) { return false; }
return oTarget.setItem(sKey, vValue);
},
deleteProperty: function (oTarget, sKey) {
if (sKey in oTarget) { return false; }
return oTarget.removeItem(sKey);
},
enumerate: function (oTarget, sKey) {
return oTarget.keys();
},
ownKeys: function (oTarget, sKey) {
return oTarget.keys();
},
has: function (oTarget, sKey) {
return sKey in oTarget || oTarget.hasItem(sKey);
},
defineProperty: function (oTarget, sKey, oDesc) {
if (oDesc && "value" in oDesc) { oTarget.setItem(sKey, oDesc.value); }
return oTarget;
},
getOwnPropertyDescriptor: function (oTarget, sKey) {
var vValue = oTarget.getItem(sKey);
return vValue ? {
value: vValue,
writable: true,
enumerable: true,
configurable: false
} : undefined;
},
});
/* Cookies test */
console.log(docCookies.my_cookie1 = "First value");
console.log(docCookies.getItem("my_cookie1"));
docCookies.setItem("my_cookie1", "Changed value");
console.log(docCookies.my_cookie1);