• 技术文章 >web前端 >js教程

    一起聊聊Javascript之Proxy与Reflect

    长期闲置长期闲置2022-02-09 17:27:00转载89
    本篇文章给大家带来了关于JavaScript中Proxy与Reflect的相关知识,希望对大家有帮助。

    ECMAScript 在 ES6 规范中加入了 Proxy 与 Reflect 两个新特性,这两个新特性增强了 JavaScript 中对象访问的可控性,使得 JS 模块、类的封装能够更加严密与简单,也让操作对象时的报错变得更加可控。

    Proxy

    Proxy,正如其名,代理。这个接口可以给指定的对象创建一个代理对象,对代理对象的任何操作,如:访问属性、对属性赋值、函数调用,都会被拦截,然后交由我们定义的函数来处理相应的操作,
    JavaScript 的特性让对象有很大的操作空间,同时 JavaScript 也提供了很多方法让我们去改造对象,可以随意添加属性、随意删除属性、随意更改对象的原型……但是此前 Object 类提供的 API 有许多缺点:

    Proxy 接口的出现很好地解决了这些问题:

    Proxy 接口在 JS 环境中是一个构造函数:

    ƒ Proxy ( target: Object, handlers: Object ) : Proxy

    这个构造函数有两个参数,第一个是我们要代理的对象,第二个是包含处理各种操作的函数的对象。
    下面是调用示例:

    //需要代理的目标
    var target = { msg: "I wish I was a bird!" }; 
    //包含处理各种操作的函数的对象
    var handler = {
    	//处理其中一种操作的函数,此处是访问属性的操作
    	get(target, property) {
    		//在控制台打印访问了哪个属性
    		console.log(`你访问了 ${property} 属性`);
    		//实现操作的功能
    		return target[property];
    	}
    }
    //构造代理对象
    var proxy = new Proxy( target , handler);
    //访问代理对象
    proxy.msg
    //控制台: 你访问了 msg 属性
    //← I wish I was a bird!

    在上面的例子中,先创建了一个对象,赋值给 target ,然后再以 target 为目标创建了一个代理对象,赋值给 proxy。在作为第二个参数提供给 Proxy 构造函数的对象里有属性名为“get”的属性,是一个函数,“get”是 Proxy 接口一个陷阱的名称,Proxy 会参照我们作为第二个参数提供的对象里的属性,找到那些属性名与陷阱名相同的属性,自动设置相应的陷阱并把属性上的函数作为陷阱的处理函数。陷阱能够拦截对代理对象的特定操作,把操作的细节转换成参数传递给我们的处理函数,让处理函数去完成这一操作,这样我们就可以通过处理函数来控制对象的各种行为。
    在上面的例子里,构造代理对象时提供的 get 函数就是处理访问对象属性操作的函数,代理对象拦截访问对象属性的操作并给 get 函数传递目标对象请求访问的属性名两个参数,并将函数的返回值作为访问的结果。

    Proxy 的陷阱一共有13种:

    陷阱名与对应的函数参数拦截的操作操作示例
    get(target, property)访问对象属性target.property
    target[property]
    set(target, property, value, receiver)赋值对象属性target.property = value
    target[property] = value
    has(target, property)判断对象属性是否存在property in target
    isExtensible(target)判断对象可否添加属性Object.isExtensible(target)
    preventExtensions(target)使对象无法添加新属性Object.preventExtensions(target)
    defineProperty(target, property, descriptor)定义对象的属性Object.defineProperty(target, property, descriptor)
    deleteProperty(target, property)删除对象的属性delete target.property
    delete target[property]
    Object.deleteProperty(target, property)
    getOwnPropertyDescriptor(target, property)获取对象自有属性的描述符Object.getOwnPropertyDescriptor(target, property)
    ownKeys(target)枚举对象全部自有属性Object.getOwnPropertyNames(target).
    concat(Object.getOwnPropertySymbols(target))
    getPrototypeOf(target)获取对象的原型Object.getPrototypeOf(target)
    setPrototypeOf(target)设置对象的原型Object.setPrototypeOf(target)
    apply(target, thisArg, argumentsList)函数调用target(...arguments)
    target.apply(target, thisArg, argumentsList)
    construct(target, argumentsList, newTarget)构造函数调用new target(...arguments)

    在上面列出的陷阱里是有拦截函数调用一类操作的,但是只限代理的对象是函数的情况下有效,Proxy 在真正调用我们提供的接管函数前是会进行类型检查的,所以通过代理让普通的对象拥有函数一样的功能这种事就不要想啦。
    某一些陷阱对处理函数的返回值有要求,如果不符合要求则会抛出 TypeError 错误。限于篇幅问题,本文不深入阐述,需要了解可自行查找资料。

    除了直接 new Proxy 对象外,Proxy 构造函数上还有一个静态函数 revocable,可以构造一个能被销毁的代理对象。

    Proxy.revocable( target: Object, handlers: Object ) : Object
    
    Proxy.revocable( target, handlers ) → {
    	proxy: Proxy,
    	revoke: ƒ ()
    }

    这个静态函数接收和构造函数一样的参数,不过它的返回值和构造函数稍有不同,会返回一个包含代理对象和销毁函数的对象,销毁函数不需要任何参数,我们可以随时调用销毁函数将代理对象和目标对象的代理关系断开。断开代理后,再对代理对象执行任何操作都会抛出 TypeError 错误。

    //创建代理对象
    var temp1 = Proxy.revocable({a:1}, {});
    //← {proxy: Proxy, revoke: ƒ}
    //访问代理对象
    temp1.proxy.a
    //← 1
    //销毁代理对象
    temp1.revoke();
    //再次访问代理对象
    temp1.proxy.a
    //未捕获的错误: TypeError: Cannot perform 'get' on a proxy that has been revoked

    弄清楚了具体的原理后,下面举例一个应用场景。
    假设某个需要对外暴露的对象上有你不希望被别人访问的属性,就可以找代理对象作替身,在外部访问代理对象的属性时,针对不想被别人访问的属性返回空值或者报错:

    //目标对象
    var target = {
    	msg: "我是鲜嫩的美少女!",
    	secret: "其实我是800岁的老太婆!" //不想被别人访问的属性
    };
    //创建代理对象
    var proxy = new Proxy( target , {
    	get(target, property) {
    		//如果访问 secret 就报错
    		if (property == "secret") throw new Error("不允许访问属性 secret!");
    		return target[property];
    	}
    });
    //访问 msg 属性
    proxy.msg
    //← 我是鲜嫩的美少女!
    //访问 secret 属性
    proxy.secret
    //未捕获的错误: 不允许访问属性 secret!

    在上面的例子中,我针对对 secret 属性的访问进行了报错,守护住了“美少女”的秘密,让我们歌颂 Proxy 的伟大!
    只不过,Proxy 只是在程序逻辑上进行了接管,上帝视角的控制台依然能打印代理对象完整的内容,真是遗憾……(不不不,这挺好的!)

    proxy//控制台: Proxy {msg: '我是鲜嫩的美少女!', secret: '其实我是800岁的老太婆!'}

    以下是关于 Proxy 的一些细节问题:

    Reflect

    学过其他语言的人看到 Reflect 这个词可能会首先联想到“反射”这个概念,但 JavaScript 由于语言特性是不需要反射的,所以这里的 Reflect 其实和反射无关,是 JavaScript 给 Proxy 配套的一系列函数。
    Reflect 在 JS 环境里是一个全局对象,包含了与 Proxy 各种陷阱配套的函数。

    Reflect: Object
    Reflect → {
    	apply: ƒ apply(),
    	construct: ƒ construct(),
    	defineProperty: ƒ defineProperty(),
    	deleteProperty: ƒ deleteProperty(),
    	get: ƒ (),
    	getOwnPropertyDescriptor: ƒ getOwnPropertyDescriptor(),
    	getPrototypeOf: ƒ getPrototypeOf(),
    	has: ƒ has(),
    	isExtensible: ƒ isExtensible(),
    	ownKeys: ƒ ownKeys(),
    	preventExtensions: ƒ preventExtensions(),
    	set: ƒ (),
    	setPrototypeOf: ƒ setPrototypeOf(),
    	Symbol(Symbol.toStringTag): "Reflect"
    }

    可以看到,Reflect 上的所有函数都对应一个 Proxy 的陷阱。这些函数接受的参数,返回值的类型,都和 Proxy 上的别无二致,可以说 Reflect 就是 Proxy 拦截的那些操作的原本实现。

    那 Reflect 存在的意义是什么呢?
    上文提到过,Proxy 上某一些陷阱对处理函数的返回值有要求。如果想让代理对象能正常工作,那就不得不按照 Proxy 的要求去写处理函数。或许会有人觉得只要用 Object 提供的方法不就好了,然而不能这么想当然,因为某些陷阱要求的返回值和 Object 提供的方法拿到的返回值是不同的,而且有些陷阱还会有逻辑上的要求,和 Object 提供的方法的细节也有所出入。举个简单的例子:Proxy 的 defineProperty 陷阱要求的返回值是布尔类型,成功就是 true,失败就是 false。而 Object.defineProperty 在成功的时候会返回定义的对象,失败则会报错。如此应该能够看出为陷阱编写实现的难点,如果要求简单那自然是轻松,但是要求一旦复杂起来那真是想想都头大,大多数时候我们其实只想过滤掉一部分操作而已。Reflect 就是专门为了解决这个问题而提供的,因为 Reflect 里的函数都和 Proxy 的陷阱配套,返回值的类型也和 Proxy 要求的相同,所以如果我们要实现原本的功能,直接调用 Reflect 里对应的函数就好了。

    //需要代理的对象
    var target = {
    	get me() {return "我是鲜嫩的美少女!"} //定义 me 属性的 getter
    };
    //创建代理对象
    var proxy = new Proxy( target , {
    	//拦截定义属性的操作
    	defineProperty(target, property, descriptor) {
    		//如果定义的属性是 me 就返回 false 阻止
    		if (property == "me") return false;
    		//使用 Reflect 提供的函数实现原本的功能
    		return Reflect.defineProperty(target, property, descriptor);
    	}
    });
    //尝试重新定义 me 属性
    Object.defineProperty(proxy , "me", {value: "我是800岁的老太婆!"})
    //未捕获的错误: TypeError: 'defineProperty' on proxy: trap returned falsish for property 'me'
    //尝试定义 age 属性
    Object.defineProperty(proxy , "age", {value: 17})
    //← Proxy {age: 17}
    
    //使用 Reflect 提供的函数来定义属性
    Reflect.defineProperty(proxy , "me", {value: "我是800岁的老太婆!"})
    //← false
    Reflect.defineProperty(proxy , "age", {value: 17})
    //← true

    在上面的例子里,由于我很懒,所以我在接管定义属性功能的地方“偷工减料”用了 Reflect 提供的 defineProperty 函数。用 Object.defineProperty 在代理对象上定义 me 属性时报了错,表示失败,而定义 age 属性则成功完成了。可以看到,除了被报错的 me 属性,对其他属性的定义是可以成功完成的。我还使用 Reflect 提供的函数执行了同样的操作,可以看到 Reflect 也无法越过 Proxy 的代理,同时也显示出了 Reflect 和传统方法返回值的区别。

    虽然 Reflect 的好处很多,但是它也有一个问题:JS 全局上的 Reflect 对象是可以被修改的,可以替换掉里面的方法,甚至还能把 Reflect 删掉。

    //备份原本的 Reflect.get
    var originGet = Reflect.get;
    //修改 Reflect.get
    Reflect.get = function get(target ,property) {
    	console.log("哈哈,你的 get 已经是我的形状了!");
    	return originGet(target ,property);
    };
    //调用 Reflect.get
    Reflect.get({a:1}, "a")
    //控制台: 哈哈,你的 get 已经是我的形状了!
    //← 1
    //删除 Reflect 变量
    delete Reflect
    //← true
    //访问 Reflect 变量
    Reflect
    //未捕获的错误: ReferenceError: Reflect is not defined

    基于上面的演示,不难想到,可以通过修改 Reflect 以欺骗的方式越过 Proxy 的代理。所以如果你对安全性有要求,建议在使用 Reflect 时,第一时间将全局上的 Reflect 深度复制到你的闭包作用域并且只使用你的备份,或者将全局上的 Reflect 冻结并锁定引用。

    相关推荐:javascript学习教程

    以上就是一起聊聊Javascript之Proxy与Reflect的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:CSDN,如有侵犯,请联系admin@php.cn删除
    专题推荐:javascript 前端 html
    上一篇:JavaScript中两个数字怎么求最大值 下一篇:Node.js深入学习之浅析require函数中怎么添加钩子

    相关文章推荐

    • JavaScript怎么增加p元素• javascript分为什么• javascript中常量池和堆的区别是什么• JavaScript经典讲解之设计模式(实例详解)• javascript怎么修改元素节点的属性

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网