JavaScript之Proxy

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对象的任何操作都将抛出异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • defineProperty() 学习书籍《ECMAScript 6 入门 》 Proxy Proxy 用于修改某...
    Bui_vlee阅读 648评论 0 1
  • 本人自学es6已经有一段时间了,只觉得有些时候很是枯燥无味, 时而又觉得在以后的职业生涯中会很有用,因为es6的很...
    可乐_37d3阅读 1,517评论 0 0
  • Proxy Proxy用于修改某些操作的默认行为,等同于在语言层面作出修改,所以属于一种“元编程”,即对编程语言进...
    南蓝NL阅读 449评论 0 0
  • Proxy 对象 Proxy 用来修改某些默认操作,等同于在语言层面做出修改。所以属于一种元编程(meta pro...
    faremax阅读 348评论 0 0
  • 先分享个常见错误: public class A {private String aa; public A(Str...
    啊灿阅读 367评论 0 1