proxy(代理机制)_ES6笔记9

一.简介

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的实现超乎想象的长,一个防篡改对象需要考虑这么多吗?需要。而且疏忽任何一点都会引发漏洞,用代理机制重写默认行为需要非常谨慎。上面的方案仍然存在问题:

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

proxy缓存起来可以避免创建不必要的代理对象,缓存Object最简单的方式当然是WeakMap,如下:

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的属性,则返回对应的属性描述符 for the own property of this object whose key is , 否则返回undefined
[[HasProperty]] (propertyKey) Boolean 返回一个布尔值表示当前对象是否已经拥有名为propertyKey的属性(该属性可以是当前对象的也可以是继承得到的) 执行key in object时被调用
[[Get]] (propertyKey, Receiver)
any
返回当前对象名为propertyKey的属性的值,如果需要执行ES代码来(从原型链)检索该属性值,会把Receiver作为this 执行obj.attr或obj[‘attr’]时被调用
[[Set]] (propertyKey,value, Receiver)
Boolean
把名为propertyKey的属性值设置为value,如果需要执行ES代码来设置该属性值(属性来自原型链),会把Receiver作为this值。属性值设置成功返回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
创建一个对象,通过newsuper操作符调用。内部方法的第一个参数是操作符的参数列表,第二个参数是最初new操作符被应用的对象。实现了该内部方法的对象被称为constructors(构造器),函数对象不一定是构造器,这种非构造器函数对象没有[[Construct]]内部方法 执行new Type()或者super()时被调用

P.S.super关键字用来调用父类构造器(super(args))或者访问父类属性(super.attr/super.fn(args),配合class关键字定义类,详细见super

五.总结

js有内置的代理机制了,配合反射机制(虽然很弱)温水煎服

难道还会有注解?这样看来,js自动测试机器人有眉目了

参考资料

  • 《ES6 in Depth》:InfoQ中文站提供的免费电子书

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code