JavaScript之Proxy

22 Sep

Proxy代理是一个共通的概念,可以起到拦截的作用。ES6里将Proxy标准化了,提供了Proxy构造函数,用来生成Proxy实例。例如var p = new Proxy(target, handler);。参照MDN

构造函数有两个参数,第一个参数target是要拦截的对象,第二个参数是拦截函数对象。先看一个最基本的例子,感受一下:

var handler = {
    get: function(target, name){
        return name in target ? target[name] : 'No prop!';
    }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = 2;

console.log(p.a);    //1
console.log(p.b);    //2
console.log(p.c);    //No prop!

上面例子中为Object对象定义了get的拦截行为。如果对象内有该属性,就返回属性值。如果对象内没有该属性,就返回错误信息。结果一目了然,当你要get对象属性值时,会被Proxy拦截到,最终得到的是经由handler拦截函数处理后的值。小细节注意一下,如示例那样,拦截操作是在Proxy实例对象p上进行的,而非在{}对象上进行的。

Proxy的handler回调函数提供了13种拦截行为:

  • getPrototypeOf / setPrototypeOf
  • isExtensible / preventExtensions
  • ownKeys / getOwnPropertyDescriptor
  • defineProperty / deleteProperty
  • get / set / has
  • apply / construct

getPrototypeOf / setPrototypeOf

handler.getPrototypeOf(target)可以拦截取对象的原型对象的行为:

Object.getPrototypeOf()
Reflect.getPrototypeOf()
__proto__
Object.prototype.isPrototypeOf()
Instanceof

参数target即想获取它原型对象的对象。返回值是返回该原型对象或null。参照MDN。例如:

var proto = {};
var p = new Proxy({}, {
    getPrototypeOf(target) {
        return proto;
    }
});
console.log(Object.getPrototypeOf(p) === proto);    // true

handler.setPrototypeOf(target, prototype)可以拦截变更对象的原型对象的行为:

Object.setPrototypeOf()
Reflect.setPrototypeOf()

参数target是目标对象,参数prototype是给目标对象设置的原型对象或null。返回值如果目标对象的原型对象被成功改变,返回true,否则返回false。参照MDN。例如:

var handler = {
    setPrototypeOf (target, prototype) {
        return false;
    }
};
var newProto = {};
var p = new Proxy({}, handler);
console.log(Object.setPrototypeOf(p, newProto));
//TypeError: proxy setPrototypeOf handler returned false

console.log(Reflect.setPrototypeOf(p, newProto));   //false

isExtensible / preventExtensions

handler.isExtensible(target)可以拦截判断对象是否可扩展(即是否能追加新属性)的行为:

Object.isExtensible()
Reflect.isExtensible()

参数target是目标对象。返回值如果目标对象可扩展,返回true,否则返回false。参照MDN。例如:

var p = new Proxy({}, {
    isExtensible: function(target) {
        console.log("called");
        return true;
    }
});

console.log(Object.isExtensible(p)); 
//called
//true

handler.preventExtensions(target)可以拦截阻止对象被扩展(即不能为对象增加新属性,但是既有属性的值仍然可以更改,也可以把属性删除)的行为:

Object.preventExtensions()
Reflect.preventExtensions()

参数target是目标对象。返回值如果想阻止对象被扩展返回true,否则返回false。但要注意只有在Object.isExtensible(proxy)为false时,才能返回true,否则会报错。参照MDN。例如:

var obj = {};
var p = new Proxy(obj, {
    preventExtensions: function(target) {
        console.log(Object.isExtensible(target));
        return true;
    }
});
console.log(Object.preventExtensions(p));
//true
//TypeError: proxy can't report an extensible object as non-extensible

因为Object.isExtensible(target);返回ture,表示对象可扩展,此时你拦截preventExtensions并返回true的话会报错,无法阻止一个可扩展对象进行扩展。所以通常应该在handler.preventExtensions里调用Object.preventExtensions来阻止对象的可扩展性,让Object.isExtensible(target);返回false:

var obj = {};
obj.newProp = 1;
console.log(obj.newProp);    //1

var p = new Proxy(obj, {
    preventExtensions: function(target) {
        Object.preventExtensions(target);
        console.log(Object.isExtensible(target));
        return true;
    }
});
console.log(Object.preventExtensions(p));
//false
//Object {}

obj.newProp2 = 2;
console.log(obj.newProp2);    //undefined

ownKeys / getOwnPropertyDescriptor

handler.ownKeys(target)可以拦截获取属性名的行为:

Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()

参数target是目标对象。返回一个数组包含对象所有自身的属性,而Object.keys()仅返回对象可遍历的属性。参照MDN。例如拦截前缀为下划线的属性名:

let person = {
    _age: 33,
    _location: 'shanghai',
    name: 'Jack'
};
let handler = {
    ownKeys (target) {
        return Reflect.ownKeys(target).filter(key => key[0] !== '_');
    }
};
let p = new Proxy(person, handler);
for (let key of Object.keys(p)) {
    console.log(person[key]);
}
//Jack

handler.getOwnPropertyDescriptor(target, prop)可以拦截获取自身属性描述的行为:

Object.getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor()

参数target是目标对象,参数prop是自身的属性名。返回该属性的描述或undefined。参照MDN。例如拦截获取前缀为下划线的属性并返回undefined:

var handler = {
    getOwnPropertyDescriptor (target, key) {
        if (key[0] === '_') {
            return;
        }
        return Object.getOwnPropertyDescriptor(target, key);
    }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'));     //undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'));    //undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'));       
//{ value: 'tar', writable: true, enumerable: true, configurable: true }

defineProperty / deleteProperty

handler.defineProperty(target, property, descriptor)可以拦截定义属性的行为:

Object.defineProperty()
Reflect.defineProperty()

参数target是目标对象,参数property是属性名,参数descriptor是属性描述符。返回值如果该属性被定义成功,返回true,否则返回false。参照MDN。例如:

var obj = {};
var p = new Proxy(obj, {
    defineProperty: function(target, prop, descriptor) {
        console.log("called: " + prop);
        Object.defineProperty(target, "a", desc)
        return true;
    }
});

var desc = { configurable: true, enumerable: true, value: 10 };
console.log(Object.defineProperty(p, "a", desc));
//called: a
//Object { a=10 }
console.log(obj.a); //10

handler.deleteProperty(target, property)可以拦截delete行为:

Property deletion: delete proxy[foo] and delete proxy.foo
Reflect.deleteProperty()

参数target是目标对象,参数property是要删除的属性名。返回值如果该属性被删除成功,返回true,否则返回false。参照MDN。例如不允许删除前缀为下划线的属性:

var handler = {
    deleteProperty (target, key) {
        invariant(key, 'delete');
        return true;
    }
};
function invariant (key, action) {
    if (key[0] === '_') {
        throw new Error(`Invalid attempt to ${action} private "${key}" property`);
    }
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop;  //Error: Invalid attempt to delete private "_prop" property

get / set / has

handler.get(target, property, receiver)可以拦截读取对象属性值的行为:

Property access: proxy[foo]and proxy.bar
Inherited property access: Object.create(proxy)[foo]
Reflect.get()

参数target是目标对象,参数property是属性名,参数receiver是一个可选对象,有时我们必须要搜索几个对象,可能是一个在receiver原型链上的对象。返回值就是属性值。参照MDN。例如:

var person = {
    name: "Jack"
};
var p = new Proxy(person, {
    get: function(target, prop, receiver) {
        if (prop in target) {
            return target[prop];
        } else {
            throw new ReferenceError("Property \"" + prop + "\" does not exist.");
        }
    }
});

console.log(p.name);    //Jack
console.log(p.age);     //ReferenceError: Property "age" does not exist.

handler.set(target, property, value, receiver)可以拦截设置对象属性值的行为:

Property assignment: proxy[foo] = bar and proxy.foo = bar
Inherited property assignment: Object.create(proxy)[foo] = bar
Reflect.set()

参数target是目标对象,参数property是属性名,参数value是属性值,参数receiver是一个可选对象,有时我们必须要搜索几个对象,可能是一个在receiver原型链上的对象。返回值如果设值成功,返回true,否则返回false。参照MDN。例如:

var handler = {
    set: function(obj, prop, value) {
        if (prop === 'age') {
            if (!Number.isInteger(value)) {
                throw new TypeError('The age is not an integer');
            }
        }
        obj[prop] = value;
        return true;
    }
};
var p = new Proxy({}, handler);
p.age = 100;
console.log(p.age);  //100
p.age = 'Jack';      //TypeError: The age is not an integer

示例中,如果值非数字则直接抛出异常。利用set方法,可以实现数据绑定,当值发生变化时,自动更新DOM。

因为get和set方法比较常用,再举个例子。例如私有属性可以在属性名请加上_下划线,但这只是潜规则,外部仍旧能畅通无阻地读写这些属性。现在用get和set方法来真正阻止外部读写带下划线的属性:

var handler = {
    get (target, property) {
        invariant(property, 'get');
        return target[property];
    },
    set (target, property, value) {
        invariant(property, 'set');
        return true;
    }
};
function invariant (property, action) {
    if (property[0] === '_') {
        throw new Error(`Invalid attempt to ${action} private "${property}" property`);
    }
}
var target = {};
var p = new Proxy(target, handler);
p._prop;         //Error: Invalid attempt to get private "_prop" property
p._prop = 'c';   //Error: Invalid attempt to set private "_prop" property

handler.has(target, prop)可以拦截检查是否含有该参数的in行为:

Property query: foo in proxy
Inherited property query: foo in Object.create(proxy)
with check: with(proxy) { (foo); }
Reflect.has()

参数target是目标对象,参数prop是属性名。返回值如果含有该属性,返回true,否则返回false。参照MDN。例如用has方法隐藏带下划线前缀的属性,不让其被in运算符发现:

var handler = {
    has (target, key) {
        if (key[0] === '_') {
            return false;
        }
        return key in target;
    }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy);    //false

如果原对象不可扩展,用has拦截会报错。

var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
    has: function(target, prop) {
        return false;
    }
});

"a" in p; 
//TypeError: proxy can't report an existing own property as non-existent on a non-extensible object

注意,has方法拦截的是hasProperty操作,而不是hasOwnProperty操作,即has方法不care该属性是对象自身的属性,还是继承来的属性。另外,虽然for…in循环也用到了in运算符,但是Chrome55,Firefox49,Opera39上试下来,for…in里并不触发has拦截。

apply / construct

Proxy不止可以拦截对象的操作还能用这两个方法拦截函数。

handler.apply(target, thisArg, argumentsList)可以拦截函数调用的行为,包括apply调用,call调用:

proxy(…args)
Function.prototype.apply() and Function.prototype.call()
Reflect.apply()

参数target是函数对象,参数thisArg是函数对象的this,参数argumentsList是函数参数。返回值可返回任意东西。参照MDN。例如:

var target = function () { return 'I am the target'; };
var handler = {
    apply: function () {
        return 'I am proxy';
    }
};
var p = new Proxy(target, handler);
console.log(p());    //I am the proxy

再看看apply和call的拦截:

var twice = {
    apply (target, ctx, args) {
        return Reflect.apply(...arguments) * 2;
    }
};
function sum (left, right) {
    return left + right;
};
var proxy = new Proxy(sum, twice);
console.log(proxy(1, 2));                //6
console.log(proxy.call(null, 3, 4));     //14
console.log(proxy.apply(null, [5, 6]));  //22

handler.construct(target, argumentsList, newTarget)可以拦截new命令:

new proxy(…args)
Reflect.construct()

参数target是目标对象,参数argumentsList是构造函数参数,参数newTarget。返回new后的对象,注意必须是对象,否则例如返回数字会报错。参照MDN。例如:

var p = new Proxy(function() {}, {
    construct: function(target, args) {
        console.log('called: ' + args.join(', '));
        return { value: args[0] * 10 };
    }
});
console.log(new p(1).value);
//called: 1
//10

同一个拦截器函数,可以同时设置多个上面介绍的13种拦截方法:

var handler = {
    get: function(target, name) {
        if (name === 'prototype') {
            return Object.prototype;
        }
        return 'Hello, ' + name;
    },
    apply: function(target, thisBinding, args) {
        return args[0];
    },
    construct: function(target, args) {
        return {value: args[1]};
    }
};
var fproxy = new Proxy(function(x, y) {
    return x + y;
}, handler);
console.log(fproxy(1, 2));     //1
console.log(new fproxy(1,2));  //Object { value=2}
console.log(fproxy.prototype === Object.prototype);   //true
console.log(fproxy.foo);       //Hello, foo

Proxy.revocable()

上面介绍的都是handler对象的方法。Proxy自身还有个静态方法Proxy.revocable(target, handler),用于创建并返回一个可取消的Proxy对象。返回的这个可取消的Proxy对象有两个属性:proxy和revoke

属性proxy会调用new Proxy(target, handler)创建一个新的Proxy对象。属性revoke是一个无参函数,用于取消,即让该Proxy对象无效。例如:

var revocable = Proxy.revocable({}, {
    get: function(target, name) {
        return "[[" + name + "]]";
    }
});
var p = revocable.proxy;
console.log(p.foo); // "[[foo]]"

revocable.revoke();

console.log(p.foo);  //TypeError: illegal operation attempted on a revoked proxy
p.foo = 1;           //TypeError: illegal operation attempted on a revoked proxy
delete p.foo;        //TypeError: illegal operation attempted on a revoked proxy
console.log(typeof p); //object

示例中Proxy.revocable方法返回一个可取消的Proxy对象。调用该对象的proxy属性得到真实的Proxy对象。如果不想用了,可以调用revoke()方法将该Proxy对象无效化。之后对Proxy对象的任何操作都将抛出异常。

Leave a Reply

Your email address will not be published. Required fields are marked *