Proxy 与 Reflect

Proxy 对象

Proxy 用来修改某些默认操作,等同于在语言层面做出修改。所以属于一种元编程(meta programming), 即对编程语言进行编程。字面理解为Proxy代理了某些默认的操作。
其使用格式如下:

var proxy = new Proxy(target, handler);

target是被代理的目标对象,handler也是个对象,用来定制拦截行为,内部定义每个被代理的行为。
注意:

  • 如果希望这个代理有效,需要在 proxy 对象上调用属性方法,而不是在 target 上调用
  • 如果指定 handler 为空对象,那么得到对象和原对象一样
  • 得到的 proxy 是 target 的引用,如果没有代理,在 proxy 上的修改和在 target 上的修改等同

看一个简单的实例

var proxy = new Proxy({},{
  get: function(target, key){
    return 35;
  }
});
console.log(proxy.time);    //35
console.log(proxy.name);    //35
console.log(proxy.title);    //35
//被代理的对象无论输入什么属性都返回35

实际上,proxy 对象也可以被继承:

var proxy = new Proxy({},{
  get: function(target, key){
    return 35;
  }
});
var obj = Object.create(proxy);
obj.time = 20;
console.log(obj.time);    //20
console.log(obj.name);    //35

感受一下它的威力:

var obj = new Proxy({}, {
  get: function(target, key, receiver){
    console.log(`getting ${key} ...`);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver){
    console.log(`setting ${key} ...`);
    return Reflect.set(target, key, value, receiver);
  }
});

obj.count = 1;            //setting count ...
++obj.count;              //getting count ...
                          //setting count ...
console.log(obj.count);   //getting count ...
                          //2

可以看出来,handler对象中 get 方法表示属性的访问请求,set 方法表示属性的写入请求。
当然不仅仅 get 和 set, 我们可以定义以下拦截函数:

  • get(target, propKey, receiver = target)
    拦截对象的读取属性。当 target 对象设置了 propKey 属性的 get 函数时,receiver 绑定 get 函数的 this。返回值任意
  • set(target, propKey, value, receiver = target)
    拦截对象的写入属性。返回一个布尔值
  • has(target, propKey)
    拦截 propKey in proxy 操作符,返回一个布尔值
  • deleteProperty(target, propKey)
    拦截 delete proxy[propKey] 操作符,返回一个布尔值
  • enumerate(target)
    拦截 for(let i in proxy) 遍历器,返回一个遍历器
  • hasOwn(target, propKey)
    拦截 proxy.hasOwnProperty('foo'),返回一个布尔值
  • ownKeys(target)
    拦截 Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy),返回一个数组。该方法返回对象所有自身属性,包括不可遍历属性,不包括 Symble属性,但是Object.keys(proxy)不应该包括不可遍历属性
  • getOwnPropertyDescriptor(target, propKey)
    拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回其属性描述符
  • defineProperty(target, propKey, propDesc)
    拦截 Object.defineProperty(proxy, propKey, propDesc), Object.defineProperties(proxy, propDesc),返回一个布尔值
  • preventExtensions(target)
    拦截 Object.preventExtensions(proxy),返回一个布尔值
  • getPrototypeOf(target)
    拦截 Object.getPrototypeOf(proxy),返回一个对象
  • isExtensible(target)
    拦截 Object.isExtensible(proxy),返回一个布尔值
  • setPrototypeOf(target, proto)
    拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值
  • apply(target, object, args)
    拦截对 proxy 实例的函数操作,包括 proxy(...args),proxy.call(object, ...args),proxy.apply(object, args)
  • construct(target, args, proxy)
    拦截用 new 调用 proxy 函数的操作,construct()返回的不是对象会报错

以下列举一些 Proxy 的实例

访问对象不存在的属性报错

var obj = new Proxy({}, {
  get: function(target, key){
    if(key in target){
      return Reflect.get(target, key);
    } else {
      throw new ReferenceError(`"${key}" is not in object`);
    }
  }
});
obj.look = "picture";
console.log(obj.look);     //"picture"
console.log(obj.sleep);    //ReferenceError: "sleep" is not in object

数组索引为负时返回倒数位置的值

var origin = [10,20];
var arr = new Proxy(origin, {
  get(target, key){
    let index = parseInt(key);
    if(index < 0){
      index = target.length + index;
      if(index < 0) return undefined;
    }
    return Reflect.get(target, index);
  }
});
console.log(arr[0]);     //10
console.log(arr[1]);     //20
console.log(arr[2]);     //undefined
console.log(arr[-1]);    //20
console.log(arr[-4]);    //undefined

保护对象内以 "_" 开头的属性为私有属性:

var o = {
  "_name": "Bob",
  "age": 13,
  "_fun": function(){
    console.log("_fun is called");
  }
};
var obj = new Proxy(o, {
  get(target, key){
    if(key.charAt(0) === '_'){
      return undefined;
    }
    return Reflect.get(target, key);
  },
  set(target, key, value){
    if(key.charAt(0) === '_'){
      throw new Error('Cannot define a property begin with "_"');
    }
    return  Reflect.set(target, key, value);
  },
  has(target,key){
    if(key.charAt(0) === '_'){
      return false;
    }
    return Reflect.has(target, key);
  },
  deleteProperty(target,key){
    if(key.charAt(0) === '_'){
      return false;
    } else {
      Reflect.deleteProperty(..arguments);
    }
  },
  apply(target,ctx,args){
    if(target.name.charAt(0) === '_'){
      throw new TypeError(`${target.name} is not defined`);
    } else {
      Reflect apply(...arguments);
    }
  },
  defineProperty(target,key,desc){
    if(key.charAt(0) === '_'){
      return new Error(`cannot define property begin with "_"`);
    } else {
      Reflect.defineProperty(..arguments);
    }
  },
  setPrototypeOf(target,proto){
    throw new TypeError(`Cannot change the proto of ${target}`);
  },
  construct(target,ctx,args){
    if(target.name.charAt(0) === '_'){
      throw new TypeError(`${target.name} is not defined`);
    } else {
      Reflect construct(...arguments);
    }
  }
});

console.log(obj.age);    //13
obj.age = 20;
console.log(obj.age);    //20
console.log(obj._name);  //undefined
obj._hobby = "Coding";   //Error: Cannot define a property begin with "_"
_name in key             //false
delete obj._name;
Object.defineProperty(obj,"_hobby",{
  value: "Coding"
});
Object.defineProperties(obj,{
  '_hobby': {
    value: "Coding"
  }
});
obj._fun();
var a = new obj._fun();
obj.__proto__ = {};     //Cannot define a property begin with "_"
Object.setPrototypeOf(obj,{})    //Cannot change the proto of obj

当然不是所有 proxy 代理都不可取消,下面方法设置的代理是可以通过定义代理时返回的revoke函数取消:

var a = {
  name:"Bob"
};
var {proxy, revoke} = Proxy.revocable(a, {
  get(target,key){
    return undefined;
  }
});
proxy.name;   //undefined;
revoke();
proxy.name;   //TypeError: Cannot perform 'get' on a proxy that has been revoked

Reflect 对象

Reflect 对象有一下作用:

  1. 将 Object对象的一些明显属于语言层面的方法部署在 Reflect 上
  2. 修改某些 Object 对象的方法使其更合理。比如 Object.defineProperty 遇到无法定义属性时会抛出错误,而 Reflect.defineProperty 会返回 false
  3. 把所以 object 的操作都替换成函数行为,比如用 Reflect.has(obj,name) 替换 name in obj
  4. 保证只要是 Proxy 有的方法就一定可以在 Reflect 上找到相同的方法,这样可以在实现 proxy 时方便的完成默认行为。换言之,无论 proxy 怎么修改默认行为,你总可以在 Reflect 上找到真正默认的行为

代理在添加额外的功能时,利用 Reflect 保证了原始功能的实现。举个例子:

var loggedObj = new Proxy({}, {
  get(target,propKey){
    console.log(`getting ${target}.${propKey}`);  //当然你最好把操作记录到一个 log 中
    return Reflect.get(target,propKey);
  }
});

Reflect有以下方法:

  • Reflect.getOwnPropertyDescriptor(target, propKey)
    等同于 ObjectgetOwnPropertyDescriptor(target, propKey)
  • Reflect.defineProperty(target,propKey,desc)
    等同于 Object.defineProperty(target,propKey,desc)
  • Reflect.getOwnPropertyNames(target)
    等同于 Object.getOwnPropertyNames(target)
  • Reflect.getPrototypeOf(target)
    等同于 Object.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, proto)
    等同于 Object.setPrototypeOf(target, proto)
  • Reflect.deleteProperty(target, propKey)
    等同于 delete target.propKey
  • Reflect.enumerate(target)
    等同于 for ... in target
  • Reflect.freeze(target)
    等同于 Object.freeze(target)
  • Reflect.seal(target)
    等同于 Object.seal(target)
  • Reflect.preventExtensions(target)
    等同于 Object.preventExtensions(target)
  • Reflect.isFrozen(target)
    等同于 Object.isFrozen(target)
  • Reflect.isSealed(target)
    等同于 Object.isSealed(target)
  • Reflect.isExtensible(target)
    等同于 Object.isExtensible(target)
  • Reflect.has(target, propKey)
    等同于 propkey in object
  • Reflect.hasOwn(target, propKey)
    等同于 target.hasOwnProperty(propKey)
  • Reflect.ownKeys(target)
    遍历得到target自身所有属性,包括不可枚举属性,不包括 Symbol 属性
  • Reflect.get(target,propKey, receiver = target)
    如果 propKey 是个读取器,则读取器中的 this 绑定到 receiver
var per = {
  bar: function(){console.log("per-bar")}
}
var obj = {
  get foo(){ this.bar(); },
  bar: function (){console.log("obj-bar")}
};
Reflect.get(obj, "foo", per);    //"per-bar"
  • Reflect.set(target,propKey, value, receiver = target)
    如果 propKey 是个读取器,则读取器中的 this 绑定到 receiver
  • Reflect.apply(target, thisArg, args)
    等同于 Function.prototype.apply.call(target, thisArg, args)thisArg.target(args)
  • Reflect.construct(target,args)
    等同于 new target(...args)

注意以上方法中,Reflect.set(), Reflect.defineProperty(), Reflect.freeze(), Reflect.seal(), Reflect.preventExtensions() 在成功时返回 true, 失败时返回 false。对应的 Object 方法失败时会抛出错误。

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

推荐阅读更多精彩内容

  • defineProperty() 学习书籍《ECMAScript 6 入门 》 Proxy Proxy 用于修改某...
    Bui_vlee阅读 644评论 0 1
  • ECMAScript发展历史 (1)ECMA-262 第1版:去除了对针对浏览器的特性,支持Unicode标准(多...
    congnie116阅读 1,847评论 0 2
  • Proxy Proxy用于修改某些操作的默认行为,等同于在语言层面作出修改,所以属于一种“元编程”,即对编程语言进...
    南蓝NL阅读 448评论 0 0
  • 很有意思的一章 Proxy get 利用Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属...
    KeithFu阅读 635评论 0 0
  • 旅行,就像在人生轨道中的一个小叉口,悠悠荡荡一圈之后带着更多的感悟再继续前往人生轨道那个未知的终点 选择去成都没有...
    崔催脆阅读 338评论 0 2