ホームページ  >  記事  >  ウェブフロントエンド  >  Javascript のプロキシとリフレクトについて話しましょう

Javascript のプロキシとリフレクトについて話しましょう

WBOY
WBOY転載
2022-02-09 17:27:002529ブラウズ

この記事では、JavaScript のプロキシとリフレクトに関する関連知識を提供します。お役に立てば幸いです。

Javascript のプロキシとリフレクトについて話しましょう

ECMAScript は、Proxy と Reflect という 2 つの新しい機能を ES6 仕様に追加しました。これら 2 つの新しい機能は、JavaScript でのオブジェクト アクセスの制御性を強化し、JS モジュール、クラスのカプセル化を作成します。より厳密かつシンプルにすることができ、オブジェクトを操作する際のエラー報告もより制御しやすくなります。

プロキシ

プロキシは、その名前が示すように、プロキシです。このインターフェイスは、指定されたオブジェクトのプロキシ オブジェクトを作成できます。プロパティへのアクセス、プロパティへの値の割り当て、関数呼び出しなど、プロキシ オブジェクトに対する操作はすべてインターセプトされ、定義した関数に渡されて、
JavaScript の特性により、オブジェクトに多くの操作の余地が与えられます。同時に、JavaScript はオブジェクトを変換するためのメソッドも多数提供します。属性を自由に追加したり、属性を削除したりすることができます。オブジェクトのプロトタイプを自由に変更できます...しかし、Object クラスによって以前に提供されていた API には多くの欠点があります:

  • Object.defineProperty を使用して特定の名前のコレクション内のすべてのプロパティを定義したい場合、列挙を通じてすべてのプロパティに対してゲッターとセッターを設定することしかできません。また、ゲッターとセッターを設定できるのは各プロパティに対してのみであるため、大きすぎるコレクションを含む関数を作成すると、パフォーマンスの問題が発生する可能性があります。
  • Object.defineProperty プロパティを定義した後、通常のアクセス機能が必要な場合は、オブジェクトの別のプロパティ名にデータを保存するか、データを保存する別のオブジェクトが必要になるだけです。監視したいプロパティは特に不便です。
  • Object.defineProperty は、配列の長さプロパティなど、クラス内の再定義不可能なプロパティを変更できません。
  • まだ存在しないプロパティや名前の予測が難しいプロパティの場合、Object.defineProperty は役に立ちません。
  • プロパティ名の列挙やオブジェクト プロトタイプの変更など、特定の動作は変更または防止できません。

プロキシ インターフェイスの登場により、これらの問題は非常にうまく解決されます。

  • プロキシ インターフェイスは、オブジェクトに対するすべての操作をいくつかのカテゴリに分類し、プロキシ トラップを通じてそれらを提供します。特定の操作をインターセプトし、定義した処理関数で論理的な判断を実行して、複雑な機能を実装し、以前よりも多くの動作を制御します。
  • プロキシによって作成されたプロキシ オブジェクトは仲介者の形式で存在し、データを保存する責任はありません。外部ユーザーが制御下にある元のオブジェクトにアクセスできるように、プロキシ オブジェクトを外部ユーザーに提供するだけで済みます。プロキシ オブジェクトのそれです。

プロキシ インターフェイスは、JS 環境のコンストラクターです:

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

このコンストラクターには 2 つのパラメーターがあります。最初のパラメーターはプロキシするオブジェクトで、2 番目のパラメーターにはオブジェクトが含まれますさまざまな操作を処理する関数のことです。
以下は呼び出し例です:

//需要代理的目标
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!

上記の例では、最初にオブジェクトが作成されてターゲットに割り当てられ、次にターゲットをターゲットとしてプロキシ オブジェクトが作成され、プロキシに割り当てられます。 Proxy コンストラクターに 2 番目のパラメーターとして指定されたオブジェクトには、関数である「get」という名前の属性があります。「get」は、Proxy インターフェイスのトラップの名前です。Proxy は、指定した属性を次のように参照します。 2 番目のパラメータ オブジェクト内の属性について、トラップ名と同じ属性名を持つ属性を検索し、対応するトラップを自動的に設定し、その属性に対する関数をトラップ処理関数として使用します。トラップはプロキシ オブジェクト上の特定の操作をインターセプトし、操作の詳細をパラメーターに変換して処理関数に渡し、処理関数が操作を完了できるようにします。これにより、処理関数を通じてオブジェクトのさまざまな動作を制御できるようになります。
上記の例では、プロキシ オブジェクトの構築時に提供された get 関数は、オブジェクトのプロパティへのアクセス操作を処理する関数です。プロキシ オブジェクトは、オブジェクトのプロパティへのアクセス操作をインターセプトし、 ターゲット オブジェクト # を渡します。 ## と ## を get 関数に #アクセスを要求する属性名 #2 つのパラメータと、関数の戻り値がアクセスの結果として使用されます。 プロキシ トラップには 13 種類があります:

#トラップ名と対応する関数パラメータインターセプトされた操作操作の例##get(target, property)target.property#set(ターゲット、プロパティ、値、受信者)##has(target, property)Object.isExtensible(target)Object.preventExtensions(target)Object.defineProperty(ターゲット、プロパティ、記述子)オブジェクトのプロパティを削除しますordelete target[プロパティ]オブジェクトのすべての独自プロパティを列挙しますconcat(Object.getOwnPropertySymbols(target) )getPrototypeOf(target)オブジェクトのプロトタイプを設定します

在上面列出的陷阱里是有拦截函数调用一类操作的,但是只限代理的对象是函数的情况下有效,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 的一些细节问题:

  • Proxy 在处理属性名的时候会把除 Symbol 类型外的所有属性名都转化成字符串,所以处理函数在判断属性名时需要尤其注意。
  • 对代理对象的任何操作都会被拦截,一旦代理对象被创建就没有办法再修改它本身。
  • Proxy 的代理是非常底层的,在没有主动暴露原始目标对象的情况下,没有任何办法越过代理对象访问目标对象(在控制台搞骚操作除外)。
  • Proxy 代理的目标只能是对象,不能是 JavaScript 中的原始类型。

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学习教程

オブジェクト プロパティへのアクセス ortarget[property ]
割り当てオブジェクトのプロパティ ターゲット。 property = value または target[property] = value
オブジェクト プロパティが存在するかどうかを確認します##ターゲットのプロパティ #isExtensible(target)Determineオブジェクトが属性を追加できるかどうか
preventExtensions(target) オブジェクトに新しいプロパティを追加できないようにします
defineProperty(target, property 、記述子) #オブジェクトのプロパティを定義します
## deleteProperty(target, property) ##delete target.property
または Object.deleteProperty(ターゲット, プロパティ) ##getOwnPropertyDescriptor(ターゲット, プロパティ)
独自のプロパティのオブジェクト記述子の取得
Object.getOwnPropertyDescriptor(target, property)
ownKeys(target) Object.getOwnPropertyNames(target).
オブジェクトのプロトタイプを取得します
Object.getPrototypeOf (target)
setPrototypeOf(target) Object .setPrototypeOf(target)
apply(target, thisArg, argumentList)#関数呼び出し #target(...arguments) ortarget.apply(target, thisArg, argumentList)
construct(target, argumentList 、newTarget) コンストラクター呼び出し new target(...arguments)

以上がJavascript のプロキシとリフレクトについて話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。