본문으로 건너뛰기

proxy(프록시 메커니즘)_ES6 노트 9

무료2016-07-22#JS#js proxy#js代理#es6 proxy#es6代理

js 에는 내장된 프록시 메커니즘이 있습니다. 리플렉션 메커니즘 (비록 매우 약하지만) 과 함께 따뜻하게 달여 복용

一.简介

new Proxy(target, handler) 구문은 핸들러 오브젝트 handler 가 타겟 오브젝트 target 의 프로퍼티 액세스 방법을 인터셉트하는 것을 허용합니다

프록시 메커니즘은오브젝트의 14 개 내부 메서드 재작성을 서포트합니다. 예를 들어 [[Get]](key, receiver), [[Set]](key, value, receiver) 등입니다. 그 중에서 receiver 는 가장 먼저 검색을 시작하는 오브젝트입니다 (액세스하고 싶은 프로퍼티는 프로토타입 체인 상에 있을 수 있습니다)

먼저 proxy 를 생성합니다:

var obj = {
    key: 'value'
};
var handler = {
    get: function(target, key, receiver) {
        console.log(`proxy key = ${key}`);
        return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
        console.log(`proxy key = ${key}, value = ${value}`);
        return Reflect.set(target, key, value, receiver);
    }
    // , apply
    // , construct
    // , defineProperty
    // , deleteProperty
    // , enumerate
    // , getOwnPropertyDescriptor
    // , getPrototypeOf
    // , has
    // , isExtensible
    // , ownKeys
    // , preventExtensions
    // , setPrototypeOf
};
var proxy = new Proxy(obj, handler);

그 후 시험해 봅니다:

// test
// 타겟 오브젝트 obj 에서 초기 상태를 복사
console.log(proxy); // Object { key: "value" }
proxy.a = 1;    // proxy key = a, value = 1
proxy['b'] = 2; // proxy key = b, value = 2

proxy.b++;
// log print:
// proxy key = b
// proxy key = b, value = 3

// 프록시 오브젝트 proxy 의 상태는 타겟 오브젝트 proxy 와 완전히 일치
// 모든 내부 메서드가 전달되기 때문
console.log(`proxy.key = ${proxy.key}, proxy.a = ${proxy.a}, proxy.b = ${proxy.b}`);
console.log(`obj.key = ${obj.key}, obj.a = ${obj.a}, obj.b = ${obj.b}`);
// log print:
// proxy key = key
// proxy key = a
// proxy key = b
// proxy.key = value, proxy.a = 1, proxy.b = 3
// obj.key = value, obj.a = 1, obj.b = 3

// 逆向
obj.x = 2;
console.log(`obj.x = ${obj.x}, proxy.x = ${proxy.x}`);
// log print:
// proxy key = x
// obj.x = 2, proxy.x = 2

참조를 복사하는 (var proxy = obj;) 것과 마찬가지로, objproxy 의 상태는 완전히 일치하며, 양방향 동기화입니다. 유일한 차이는 handler 가 몰래 무언가를 했다는 것입니다

二.特点

###1. 行為 전달과 상태 동기화

프록시의 행위: 프록시의 모든 내부 메서드를 타겟에 전달

프록시 오브젝트 proxy 와 타겟 오브젝트 target 은 상태를 일치시킵니다: 프록시 오브젝트 생성 시 target 에서 현재 상태를 복사하고, 내부 메서드는 모두 전달됩니다

###2.proxy !== target

예를 들어 targetDOMElement 이고, proxy 는 다릅니다. document.body.appendChild(proxy) 를 호출하면 TypeError 가 트리거됩니다

게다가, 위의 예에서는

console.log(obj === proxy); // false
console.log(obj == proxy);  // false

이는 매우 합리적입니다. 프록시는 본래 참조 복사와 다르기 때문입니다

###3.handler 인터셉트

handler 오브��트를 통해 14 개의 내부 메서드를 재작성하여, target 의 디폴트 행위를 인터셉트하고 변경할 수 있습니다

handler 에 의해 인터셉트되지 않은 (handler 오브젝트 중에서 재작성되지 않은) 내부 메서드는 직접 타겟을 가리킵니다

14 개의 내부 메서드는 모두 handler 오브젝트 중에서 재작성 가능합니다. 프로퍼티명은 Reflect 오브젝트의 14 개 프로퍼티명과 일치합니다. 상세는 MDN

###4. 프록시 관계는 해제 가능

프록시는 해제 가능합니다. Proxy.revocable(target, handler) 를 사용하여 Object { proxy: Object, revoke: revoke() } 를 생성하고, revoke() 를 호출하여 프록시 관계를 해제합니다. 해제 후 프록시 오브젝트에 액세스하면 에러가 발생합니다. 예를 들어:

// 解除代理关系
var rProxy = Proxy.revocable({}, {
    set: function(target, key, value, receiver) {
        return Reflect.set(target, key, 0, receiver);
    }
});
var p = rProxy.proxy;
p.a = 123;
console.log(p.a);   // 0
// 解除代理
rProxy.revoke();
// p.b = 213;  // 报错 TypeError: illegal operation attempted on a revoked proxy

###5. 오브젝트 불변성

target 이 확장 불가능이 아닌 한, 프록시 오브젝트는 확장 불가능으로 선언할 수 없습니다

三.应用场景

###1. 自動填充 오브젝트

// 1.自动填充
var Tree = function() {
    return new Proxy({}, {
        get: function(target, key, receiver) {
            if (typeof target[key] === 'undefined') {
                console.log(`${key} is undef, but it will be auto filled`);
                // 利用当前 handler 创建 proxy 作为 value
                Reflect.set(target, key, new Proxy({}, this), receiver);
                // 上面代码等价于
                // target[key] = new Proxy({}, this);
                // 也等价于
                // target[key] = Tree();
            }
            
            return Reflect.get(target, key, receiver);
        }
    });
}
var tree = Tree();
console.log(tree);  // Object { branch1: Object }
tree.branch1.branch2.twig = 'green';
// log print:
// branch1 is undef, but it will be auto filled
// branch2 is undef, but it will be auto filled
console.log(tree);  // Object { branch1: Object }
tree.branch1.branch3.twig = "yellow";
// log print:
// branch3 is undef, but it will be auto filled
console.log(tree);  // Object { branch1: Object }

유용한지 여부는 차치하고, ES6 이전 시대에는확실히 구현할 수 없었습니다自動填充 오브젝트 (소스 코드를 "컴파일"하는 흑마술 제외)

P.S.위의 log 결과는 Firefox47 에서 온 것입니다. Chrome51.0.2704.106 에서는 기이한 것이 나타났습니다:

// chrome log print:
Object {splice: Object}
splice is undef, but it will be auto filled
splice is undef, but it will be auto filled
branch1 is undef, but it will be auto filled
branch2 is undef, but it will be auto filled
Object {splice: Object, branch1: Object}
splice is undef, but it will be auto filled
branch3 is undef, but it will be auto filled
Object {splice: Object, branch1: Object}

splice 는 어디에서 온 것일까요? 게다가 splice 의 값 Object 를 확인하면, 무한히 전개할 수 있다는 것을 알 수 있습니다 (console 은 매우 길고 넓은 > 형을 표시합니다). splice 가 자신을 순환 참조하는 것처럼 느껴집니다. 일단 Chrome 구현상의bug로 생각하고, 깊이 추구할 필요는 없습니다

###2. 改ざん 방지 오브젝트

프록시 생성에는 매우 주의가 필요합니다.否则漏洞가 있습니다

function readOnly(obj) {
    var err = new Error('can\'t modify read-only object');
    return new Proxy(obj, {
        // 1.重写所有 setter
        set: function(target, key, value, receiver) {
            throw err;
        },
        IsExtensible: function(target, receiver) {
            return false;
        },
        deleteProperty: function(target, key, receiver) {
            throw err;
        },
        defineProperty: function(target, key, desc, receiver) {
            throw err;
        },
        setPrototypeOf: function(target, proto, receiver) {
            throw err;
        },

        // 2.重写所有 getter
        // getter 存在漏洞,x 只读,但 x.prop 仍然是可写的
        // 篡改 x.prop 可能影响 x 内部逻辑
        // 所以还要重写 get
        get: function(target, key, receiver) {
            var res = Reflect.get(target, key, receiver);
            // if (typeof res === 'object') {
            if (Object(res) === res) {  // 判断 res 是不是 Object 类型
                res = readOnly(res);
            }
            return res;
        },
        getPrototypeOf: this.get,
        getOwnPropertyDescriptor: this.get
    });
}
var openObj = {
    attr: 1,
    obj: {
        val: 1,
        getVal: function() {
            return this.val;
        }
    },

    fn: function() {
        console.log('fn is called');
    }
};
var closeObj = readOnly(openObj);
// 避免 throw err 中断执行流,影响测试
var nextTick = fn => setTimeout(fn, 50);
nextTick(() => console.log(closeObj.attr));
nextTick(() => closeObj.fn());
nextTick(() => closeObj.fn = console.log);
nextTick(() => delete closeObj.obj);
// 如果忘记限制 getter 就会出现 bug
nextTick(() => {
    closeObj.obj.val = 'hack';
    console.log(closeObj.obj.getVal()); // hack
})

readOnly 의 구현은 상상 이상으로 길고, 1 개의 改ざん 방지 오브젝트에 이ほど 많은 것을 고려할 필요가 있을까요? 필요합니다. 게다가任何一点을 소홀히 하면漏洞를 일으킵니다. 프록시 메커니즘으로 디폴트 행위를 재작성하려면매우 주의가 필요합니다. 위의 방안仍然存在問題:

// 访问 2 次,new2 个 proxy 对象
console.log(closeObj.obj === closeObj.obj); // false

proxy 를 캐시함으로써 불필요한 프록시 오브젝트 생성을 회피할 수 있습니다. Object 를 캐시하는 가장 간단한 방법은 물론 [WeakMap](/articles/集合(set 和 map)-es6 笔记 8/) 입니다. 다음과 같습니다:

function readOnlyEx(obj) {
    var proxyCache = new WeakMap();
    //...
    if (Object(res) === res) {  // 判断 res 是不是 Object 类型
        // 缓存
        if (!proxyCache.get(res)) {
            proxyCache.set(res, readOnly(res));
        }
        res = proxyCache.get(res);
    }
    //...
}
// test
var _closeObj = readOnlyEx(openObj);
// 同一个 proxy 对象,因为有 WeakMap
console.log(_closeObj.obj === _closeObj.obj);   // true

게다가, proxy !== target주의가 필요한문제입니다. 예를 들어 new Proxy(window, {}).alert(1) 은 에러로 실행할 수 없습니다. apply 처리가 필요합니다. 다음과 같습니다:

// proxy !== target
// new Proxy(window, {}).alert(1);
// 会报错 TypeError: 'alert' called on an object that does not implement interface Window.

var mWin = new Proxy(window, {
    get: function(target, key, receiver) {
        var res = Reflect.get(target, key, receiver);
        //! function 判断可能不够严密
        if (typeof res === 'function') {
            return function() {
                return res.apply(target, arguments);
            }
        }
    }
});
// test
mWin.alert(123);    // alert 123

###3. 検証 로직의 분리

setter/getter 의 이점은, 프록시 메커니즘을 통해 검증 로직을 프로퍼티 액세스 행위에 내장할 수 있다는 것입니다 (검증 로직을 둘 곳이 있습니다)

###4. 오브젝트 액세스의 기록

주로 테스트/디버그에서 사용됩니다. 예를 들어 테스트 프레임워크에서, 프록시 메커니즘을 통해 모든 것을 완전히 기록할 수 있습니다

###5. 通常 오브젝트의 강화

예를 들어 自動填充 오브젝트와 改ざん 방지 오브젝트로, API 설계 방면에 큰 상상 공간이 있습니다

###6.WeakMap 과의 연계

같은 프록시의 다수 생성을 회피합니다. 예를 들어 readOnlyEx 에서, WeakMap 으로 프록시 오브젝트를 관리하는 것은 매우 자연스러운 일입니다

四.오브젝트의 내부 메서드

訳自:http://www.ecma-international.org/ecma-262/6.0/index.html#table-5

Table 5 — 필요한 내부 메서드
내부 메서드시그니처설명說明
[[GetPrototypeOf]]() Object | Null현재의 오브젝트에 상속 속성을 제공하는 오브젝트를 결정하며, null 값은 상속된 속성이 없음을 나타냅니다obj.__proto__ 또는 obj['__proto__'] 또는 Object.getPrototypeOf(obj) 를 실행할 때 호출됩니다
[[SetPrototypeOf]](Object | Null) Boolean현재의 오브젝트와 상속 속성을 제공하는 다른 오브젝트와의 관련을 확립하며, null을 건네면 속성을 상속하지 않음을 나타내고, true를 반환하면 조작이 정상적으로 완료되었음을 나타내고, false 는 조작 실패를 나타냅니다
[[IsExtensible]]( ) Boolean현재의 오브젝트에 추가의 속성을 추가하는 것을 허가하는지 여부를 결정합니다
[[PreventExtensions]]( ) Boolean새로운 속성을 현재의 오브젝트에 추가할 수 있는지 여부를 제어하며, 조작 성공 시에는true 를 반환하고, 否则false 를 반환합니다
[[GetOwnProperty]](propertyKey) Undefined | Property Descriptor현재의 오브젝트가propertyKey 라는 이름의 속성을 소유하고 있는 경우, 이 오브젝트의 독자 프로퍼티에 대응하는속성 기술자 를 반환하고, 否则undefined 를 반환합니다
[[HasProperty]](propertyKey) Boolean현재의 오브젝트가 이미propertyKey 라는 이름의 속성을 소유하고 있는지 여부 (該 속성은 현재의 오브젝트의 것이어도 상속된 것이어도 좋음) 를 나타내는 불리언 값을 반환합니다key in object 를 실행할 때 호출됩니다
[[Get]](propertyKey, Receiver)
any
현재의 오브젝트의propertyKey 라는 이름의 속성의 값을 반환합니다. 該 속성 값을 (프로토타입 체인에서) 취득하기 위해 ES 코드를 실행할 필요가 있는 경우, Receiverthis 값으로 사용합니다obj.attr 또는 obj['attr'] 를 실행할 때 호출됩니다
[[Set]](propertyKey,value, Receiver)
Boolean
propertyKey 라는 이름의 속성 값을value 로 설정합니다. 該 속성 값을 설정하기 위해 ES 코드를 실행할 필요가 있는 경우 (속성은 프로토타입 체인에서), Receiverthis 값으로 사용합니다. 속성 값 설정 성공 시에는true 를 반환하고, 否则false 를 반환합니다obj.attr/obj[attr] = val 를 실행할 때 호출되며, +=,++,--를 실행할 때는 먼저 [[Get]] 을 호출한 후 [[Set]] 을 호출합니다
[[Delete]](propertyKey) Boolean현재의 오브젝트에서propertyKey 라는 이름의 속성을 삭제합니다. 속성이 삭제되지 않고 존재し続ける 경우는false 를 반환하고, 속성이 삭제되었거나 존재하지 않는 경우는true 를 반환합니다
[[DefineOwnProperty]](propertyKey, PropertyDescriptor)
Boolean
propertyKey 라는 이름의 기존 속성을 생성 또는 수정하고, PropertyDescriptor 로 기술된 상태로 설정합니다. 속성이 정상적으로 생성/업데이트된 경우에는true 를 반환하고, 否则false 를 반환합니다
[[Enumerate]]()Object현재의 오브젝트의 열거 가능 속성 키 세트를 생성할 수 있는 이터레이터 오브젝트를 반환합니다for (var key in obj) 를 실행할 때 호출되며, 내부 메서드는 반복 가능 오브젝트를 반환하고, for...in 루프는 이 내부 메서드를 통해 오브젝트 키 세트를 취득할 수 있습니다
[[OwnPropertyKeys]]()List of propertyKey현재의 오브젝트의 모든 소유 속성 (프로토타입 체인에서의 속성을 포함하지 않음) 의 키로 구성되는List 를 반환합니다
Table 6 — Function 오브젝트의 추가 필요한 내부 메서드
내부 메서드시그니처설명說明
[[Call]](any, a List of any)
any
현재의 오브젝트의 관련 코드를 실행하고, 함수 호출식을 통해 호출합니다. 내부 메서드의 파라미터는this 값과 함수 호출식을 통해 건네진 파라미터 리스트입니다. 該 내부 메서드를 실장한 오브젝트는callable(호출 가능) 입니다.fn() 또는 obj.fn() 를 실행할 때 호출됩니다
[[Construct]](a List of any, Object)
Object
오브젝트를 생성하고, new 또는 super 연산자를 통해 호출합니다. 내부 메서드의 첫 번째 파라미터는 연산자의 파라미터 리스트이고, 두 번째 파라미터는 처음에new 연산자가 적용된 오브젝트입니다. 該 내부 메서드를 실장한 오브젝트는constructors(컨스트럭터) 라고 불리며, 함수 오브젝트는 반드시 컨스트럭터는 아닙니다. 이러한 비컨스트럭터 함수 오브젝트에는 [[Construct]] 내부 메서드가 없습니다new Type() 또는 super() 를 실행할 때 호출됩니다

P.S.super 키워드는 부모 클래스 컨스트럭터 (super(args)) 를 호출하거나, 부모 클래스 속성에 액세스하는 (super.attr/super.fn(args), class 키워드와 조합하여 클래스를 정의합니다. 상세는 super) 위해 사용됩니다

五.정리

js 에는 내장된 프록시 메커니즘이 있습니다. 리플렉션 메커니즘 (비록 매우 약하지만) 과 함께 따뜻하게 달여 복용

주석이 있을까요? 이렇게 보면, js 자동 테스트 로봇에 눈썹이 섰습니다

참고 자료

  • 《ES6 in Depth》: InfoQ 中文站이 제공하는 무료 전자서적

댓글

아직 댓글이 없습니다

댓글 작성