Proxy-神奇的代理

前言


今天我们要来讲一下Javascript中一个很有趣的类 -- Proxy类,类名翻译成中文是代理的意思。在真正开始之前还必须说明一点,本篇文章并非原创,主要借鉴了先行者贾顺名的文章,在此非常感谢他(她)的分享,原文地址是https://segmentfault.com/a/1190000015009255


什么叫代理?


我就举个生活中的小例子吧,对于很多出门在外的小伙伴来说,肯定有过一个非常刺激的经历---租房子。咱们在租房子的时候很少能直接接触到房东,接触到的都是房产中介,房产中介就是房东的代理,帮忙处理租房的相关事宜。


Javascript的代理(Proxy)


Proxy是ES6中提供的新的API,用来自定义对象的各项基本操作,说白了就是在我们访问对象前添加了一层拦截器(可能过滤数据,数据验证等过程)。有些小伙伴就会有疑问了,比如说简单的赋值操作加个拦截器有啥意义吗?大家想一个简单的问题,如果一个人的年龄被设置为500合理吗?代理的设置就可以帮我们规避这种不合理的数据更改。

语法


let p = new Proxy(target, handler);

创建一个Proxy的实例需要传入两个参数:

1、target 要被代理的对象,可以是一个object或者function

2、handlers对代理对象p的各种操作的自定义处理函数

注意:在第二个参数为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝(Proxy必须是浅拷贝,如果是深拷贝则会失去了代理的意义)

Traps(各种行为的代理)


其实Javascript早就定义了两种trap:getter 和 setter(存取器属性),具体代码如下:

let obj = {

    _age: 18,

    get age () {

        return `I'm ${this._age} years old`

    },

    set age (val) {

        this._age = Number(val)

    }

}

console.log(obj.age) // I'm 18 years old

obj.age = 19

console.log(obj.age) // I'm 19 years old

以上这方式存在两个缺点:

1、针对每一个要代理的属性都要编写对应的getter、setter。

2、必须还要存在一个存储真实值的key(上面就是_age),试想如果没有_age的话,直接在age的getter里面返回this.age,这个动作又会触发新一轮的getter,导致死循环的产生。

那么Proxy如果来解决着两个问题呢?

1、首先通过创建get、set两个trap来统一管理所有的操作;

2、trap内部操作的是target对象,而不是proxy对象,无需额外的用一个key来存储真实的值

let target = {

    age: 18,

    name: 'Niko Bellic'

}

let handlers = {

    get (target, property) {

        return `${property}: ${target[property]}`

    },

    set (target, property, value) {

        target[property] = value

    }

}

let proxy = new Proxy(target, handlers)

proxy.age = 19

console.log(target.age, proxy.age) // 19, age : 19

console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

除了get和set,还有其他trap:

getPrototypeOf:当读取代理对象的原型时,该方法就会被调用;方法内部this指向handler(处理器对象);方法的返回值必须是一个对象或者 null。

在 JavaScript 中,有下面这五种操作(方法/属性/运算符)可以触发 JS 引擎读取一个对象的原型,也就是可以触发 getPrototypeOf() 代理方法的运行:

1、Object.getPrototypeOf()

2、Reflect.getPrototypeOf()

3、__proto__

4、Object.prototype.isPrototypeOf()

5、instanceOf

var obj = {};var p = new Proxy(obj, {

    getPrototypeOf(target) {

        return Array.prototype;

    }

});

console.log(

    Object.getPrototypeOf(p) === Array.prototype, // true

    Reflect.getPrototypeOf(p) === Array.prototype, // true

    p.__proto__ === Array.prototype, // true

    Array.prototype.isPrototypeOf(p), // true

    p instanceof Array // true

);

如果遇到了下面两种情况,JS 引擎会抛出TypeError 异常:

1、getPrototypeOf() 方法返回的不是对象也不是 null。

2、目标对象是不可扩展的,且 getPrototypeOf() 方法返回的原型不是目标对象本身的原型

var obj = {};var p = new Proxy(obj, {

    getPrototypeOf(target) {

        return "foo";

    }

});

Object.getPrototypeOf(p); // TypeError: "foo" is not an object or null

var obj = Object.preventExtensions({});

var p = new Proxy(obj, {

    getPrototypeOf(target) {

        return {};

    }

});

Object.getPrototypeOf(p); // TypeError: expected same prototype value

defineProperty:用于拦截对对象的Object.defineProperty()操作; this绑定在 handler 对象上; 方法必须以一个 Boolean 返回,表示定义该属性的操作成功与否。

该方法会拦截目标对象的以下操作 :

1、Object.defineProperty()

2、Reflect.defineProperty()

3、proxy.property = 'value'

如果违背了以下的不变量,proxy会抛出 TypeError:

1、如果目标对象不可扩展, 将不能添加属性。

2、不能添加或者修改一个属性为不可配置的,如果它不作为一个目标对象的不可配置的属性存在的话。

3、如果目标对象存在一个对应的可配置属性,这个属性可能不会是不可配置的。

4、如果一个属性在目标对象中存在对应的属性,那么 Object.defineProperty(target, prop, descriptor) 将不会抛出异常。

5、在严格模式下, false 作为 handler.defineProperty 方法的返回值的话将会抛出 TypeError 异常.

除此之外,当调用 Object.defineProperty() 或者 Reflect.defineProperty(),传递给 defineProperty 的 descriptor   有一个限制 - 只有以下属性才有用,非标准的属性将会被无视 : enumerable、configurable、writable、value、get、set

var p = new Proxy({}, {

    defineProperty: function(target, prop, descriptor) {

        console.log('called: ' + prop); return true;

    }

});

var desc = { configurable: true, enumerable: true, value: 10 };

Object.defineProperty(p, 'a', desc); // "called: a"


has针对 in 操作符的代理方法; this绑定在 handler 对象上; 方法返回一个 boolean 属性的值。

该方法会拦截目标对象的以下操作 :

1、属性查询: foo in proxy

2、继承属性查询: foo in Object.create(proxy)

3、with检查: with(proxy) { (foo); }

4、Reflect.has()

如果违反了下面这些规则,  proxy 将会抛出 TypeError:

1、如果目标对象的某一属性本身不可被配置,则该属性不能够被代理隐藏.

2、如果目标对象为不可扩展对象,则该对象的属性不能够被代理隐藏

var p = new Proxy({}, {

    has: function(target, prop) {

        console.log('called: ' + prop); return true;

    }

});

console.log('a' in p);


deleteProperty用于拦截对对象属性的 delete 操作; this绑定在 handler 对象上; 方法必须返回一个 Boolean 类型的值,表示了该属性是否被成功删除。

该方法会拦截以下操作:

1、删除属性: delete proxy[foo] 和 delete proxy.foo

2、Reflect.deleteProperty()

如果违背了以下不变量,proxy 将会抛出一个 TypeError:

1、如果目标对象的属性是不可配置的,那么该属性不能被删除。

var p = new Proxy({}, {

    deleteProperty: function(target, prop) {

        console.log('called: ' + prop);    

        return true;

    }

});

delete p.a;


ownKeys: 用于拦截获取自有属性操作; this绑定在 handler 对象上; 方法必须返回一个可枚举对象。

该拦截器可以拦截以下操作:

1、Object.getOwnPropertyNames()

2、Object.getOwnPropertySymbols()

3、Object.keys()

4、Reflect.ownKeys()

如果违反了下面的约定,proxy将抛出错误 TypeError:

1、ownKeys的结果必须是一个数组

2、数组的元素类型要么是一个 String ,要么是一个Symbol

3、结果列表必须包含目标对象的所有不可配置(non-configurable )、自有(own)属性的key

4、如果目标对象不可扩展,那么结果列表必须包含目标对象的所有自有(own)属性的key,不能有其它值

var p = new Proxy({}, {

    ownKeys: function(target) {

        console.log('called');

        return ['a', 'b', 'c'];

    }

});

console.log(Object.getOwnPropertyNames(p)); // "called" // [ 'a', 'b', 'c' ]


apply:  用于拦截函数的调用;

接受三个参数:target -- 目标对象(函数);thisArg -- 被调用时的上下文对象;argumentsList -- 被调用时的参数数组

返回值:可返回任何值

该方法会拦截目标对象的以下操作:

1、proxy(...args)

2、Function.prototype.apply() 和 Function.prototype.call()

3、Reflect.apply()

如果违反了以下约束,代理将抛出一个TypeError:

1、target必须是可被调用的。也就是说,它必须是一个函数对象。

var p = new Proxy(function() {}, {

    apply: function(target, thisArg, argumentsList) {

        console.log('called: ' + argumentsList.join(', '));

        return argumentsList[0] + argumentsList[1] + argumentsList[2];

    }

});

console.log(p(1, 2, 3)); // "called: 1, 2, 3" // 6

construct:  用于拦截 new 操作符. 为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的)。

接受三个参数:target -- 目标对象;argumentsList -- constructor的参数列表;newTarget -- 最初被调用的构造函数

返回值:必须返回一个对象

该方法会拦截以下操作:

1、new proxy(...args)

2、Reflect.construct()

至此,我们已经把比较常用的trap学习了一下,如果想要学习更详细的内容,请前往https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler查看

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

推荐阅读更多精彩内容

  • defineProperty() 学习书籍《ECMAScript 6 入门 》 Proxy Proxy 用于修改某...
    Bui_vlee阅读 648评论 0 1
  • Proxy 概述 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(met...
    pauljun阅读 3,249评论 0 1
  • 概述 Proxy 实例的方法 Proxy.revocable() this 问题 实例:Web 服务的客户端 1 ...
    Android_冯星阅读 668评论 0 0
  • 扬帆起航 在这波涛汹涌的大海上 帆船迎着风浪前行 狂风卷着海浪 拍打着坚硬的船体 水手们严阵以待 随时应对这无情大...
    让比耶Fonsi阅读 234评论 0 6