Proxy详解,运用与Mobx

2023-12-18 17:08
文章标签 详解 proxy 运用 mobx

本文主要是介绍Proxy详解,运用与Mobx,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文简单阐述一点元编程的知识,然后较为详细的给出 Proxy 的有关用法(起码比 MDN 详细,补充了各种错误情况的具体示例,且比上面的机翻准确),再用一些例子讲述 Proxy 适合在什么场景下使用

元编程

首先,在编程中有以下两个级别:

  • 基本级别/应用程序级别:代码处理用户输入
  • 级别:代码处理基本级别的代码

元( meta ) 这个词缀在这里的意思是:关于某事自身的某事,因此元编程( metaprogramming )这个词意味着 关于编程的编程,可以在两种同的语言进行元编程,编写元程序的语言称之为元语言。被操纵的程序的语言称之为“目标语言”,在下面这段代码中 JavaScript 为元语言,而 Java 为目标语言:

const str = 'Hello' + '!'.repeat(3);
console.log('System.out.println("'+str+'")');
复制代码

一门编程语言同时也是自身的元语言的能力称之为反射(Reflection),用于发现和调整你的应用程序结构和语义。

元编程有三种形式:

  • 自省(Introspection): 暴露出程序运行时的状态,以只读的形式获取程序结构
    • 比如使用 Object.keys(obj) 等,ES6 中新出了个 Reflect 对许多获取内部状态的接口进行了整合与统一
  • Self-modification: 可以修改运行时的程序结构/数据
    • 比如 deleteproperty descriptors
  • 调解(Intercession): 可以重新定义某些语言操作的语义
    • 比如本文中的 Proxy

元编程与 Reflect

ES6 中也新增了一个全局对象 Reflect,其中的大多数方法都早已以其他形式存在,这次将其接口统一的目的在于:

  1. 在以前各种获取/修改程序运行时状态的方法通常是散落在各处的,有的挂在 Object.prototype 上,有的挂在 Function.prototype 上,有的是一个操作符(如 delete / in 等 )
  2. 以前的调用过于复杂或不够安全
    • 某些情况下调用 obj.hasOwnProperty 时对象上可能没有这个方法(比如这个对象是通过 Object.create(null) 创建的),因此这个时候使用 Object.prototype.hasOwnProperty.call 才是最安全的,但是这样过于复杂
    • callapply 也有上述问题
  3. 返回值不科学,如使用 Object.defineProperty ,如果成功返回一个对象,否则抛出一个 TypeError ,因此不得不使用 try...catch 来捕获定义属性时发生的任何错误。而 Reflect.defineProperty 返回一个布尔值表示的成功状态,所以在这里可以只用 if...else

这里可以参考一篇文章来了解 Reflect 做出了哪些优化

Proxy 的基本内容

终于来到了我们的主角,首先我们来看看 Proxy 的构造函数:

Proxy(target, handler)
复制代码
  • target: 用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler: 处理器对象( proxy's handler)用来自定义代理对象的各种可代理操作。其中包括众多 traps

不变量(Invariants)

在进一步探究 Proxy 有什么之前,先回顾一下如何通保护对象:

  • 不可扩展(Non-extensible)
    • 不可新增属性且不可改变原型
    'use strict'const obj = Object.preventExtensions({});console.log(Object.isExtensible(obj)); // falseobj.foo = 123; // Cannot add property foo, object is not extensibleObject.setPrototypeOf(obj, null) // #<Object> is not extensible
复制代码
  • 不可写(Non-writable)
    • value 不能被赋值运算符改变
  • 不可配置(Non-configurable)
    • 不能改变/删除属性(除了把 writable 改为 false

使用代理以后,很容易违反上述约束(因为上述约束作用在被 Proxy 代理的对象中, Proxy 对象并不受其约束),因此在调用/返回的时候 Proxy 会帮我们检查或者强制做类型转换等(比如预期是 Boolean 时会把 truishfalsish 强制转换成 Boolean 等)。后文中的约束部分有进一步的解释与示例。

这里有一份关于 不变量的文档

然后来看看 Proxyhandler 提供了哪些东西供我们使用

handler.get()

拦截对象的读取属性操作。

get: function(target, property, receiver) {}
复制代码
  • target :目标对象。
  • property :被获取的属性名。
  • receiver :最初被调用的对象。通常是 proxy 本身,但 handlerget 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)。
    • 这里的 targetproperty 都很好理解,但是 receiver 需要额外注意,下面使用一个例子帮助理解:
    var obj = {myObj: 1};obj.__proto__ = new Proxy({test: 123},{get:function(target, property, receiver) {console.log(target, property, receiver);return 1;}});console.log(obj.test);// {test: 123}, "test" ,{myObj: 1}// 可以看见 receiver 是最初被调用的对象
复制代码

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

  • 访问属性: proxy[foo]proxy.bar
  • 访问原型链上的属性: Object.create(proxy)[foo]
  • Reflect.get()

约束(违反约束会抛出 Type Error):

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同。
    const obj = {};// 不可写以及不可配置Object.defineProperty(obj, "a", { configurable: false,enumerable: true, value: 10, writable: false});const p = new Proxy(obj, {get: function(target, prop) {return 20;}});console.log(p.a); // 'get' on proxy: property 'a' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '10' but got '20')
复制代码
  • 如果目标对象的属性是不可配置且没有定义其 get 方法,则其返回值必须为 undefined
    const obj = { a: 10 };// 不可配置 且 没有定义 getObject.defineProperty(obj, "a", { configurable: false,get: undefined,});const p = new Proxy(obj, {get: function(target, prop) {return 20;}});console.log(p.a) // 'get' on proxy: property 'a' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '20')
复制代码

handler.set()

拦截设置属性值的操作

set: function(target, property, value, receiver) {}
复制代码
  • target :目标对象。
  • property :被设置的属性名。
  • value :被设置的新值
  • receiver :最初被调用的对象。同上文 get 中的 receiver

返回值:

set 方法应该返回一个 Boolean :

  • 返回 true 代表此次设置属性成功了
  • 返回 false 且设置属性操作发生在严格模式下,那么会抛出一个 TypeError

注意: Proyx 中大多数方法要求返回 Boolean 时本质上是会帮你把返回值转换成 Boolean,因此可以在里面随便返回啥,到了外面拿到的都是 Boolean;这也是为什么报错的时候用词为: truishfalsish

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

  • 指定属性值: proxy[foo] = barproxy.foo = bar
  • 指定继承者的属性值: Object.create(proxy)[foo] = bar
  • Reflect.set()

约束:

  • 若目标属性是不可写及不可配置的,则不能改变它的值。
    const obj = {};// 不可写以及不可配置Object.defineProperty(obj, "a", {configurable: false,enumerable: true,value: 10,writable: false});const p = new Proxy(obj, {set: function(target, prop, value, receiver) {console.log("called: " + prop + " = " + value);return true;}});p.a = 20; // trap returned truish for property 'a' which exists in the proxy target as a non-configurable and non-writable data property with a different value// 注意这里我们并没有真正改变 'a' 的值,该错误由 return true 引起    
复制代码
  • 如果目标对象的属性是不可配置且没有定义其 set 方法,则不能设置它的值。
    const obj = {};// 不可写 且 没有定义 setObject.defineProperty(obj, "a", {configurable: false,set: undefined});const p = new Proxy(obj, {set: function(target, prop, value, receiver) {console.log("called: " + prop + " = " + value);return true;}});p.a = 20; // trap returned truish for property 'a' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter// 注意这里我们并没有真正改变 'a' 的值,该错误由 return true 引起
复制代码
  • 在严格模式下,若 set 方法返回 false ,则会抛出一个 TypeError 异常。
    'use strict'const obj = {};const p = new Proxy(obj, {set: function(target, prop, value, receiver) {console.log("called: " + prop + " = " + value);return false;}});p.a = 20; // trap returned falsish for property 'a'
复制代码

handler.apply()

拦截函数的调用

apply: function(target, thisArg, argumentsList) {}
复制代码
  • target :目标对象(函数)。
  • thisArg :被调用时的上下文对象。
  • argumentsList :被调用时的参数数组。

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

  • proxy(...args)
  • Function.prototype.apply()Function.prototype.call()
  • Reflect.apply()

约束:

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

handler.construct()

用于拦截 new 操作符

construct: function(target, argumentsList, newTarget) {}
复制代码
  • target :目标对象。
  • argumentsList :constructor 的参数列表。
  • newTarget :最初被调用的构造函数。

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

  • new proxy(...args)
  • Reflect.construct()

注意:

  • 为了使 new 操作符在生成的 Proxy 对象上生效,用于初始化代理的目标对象自身必须具有 [[Construct]] 内部方法,即 new target 必须是有效的。比如说 target 是一个 function

约束:

  • construct 方法必须返回一个对象,否则将会抛出错误 TypeError
    const p = new Proxy(function () {}, {construct: function (target, argumentsList, newTarget) {return 1;}});new p(); // 'construct' on proxy: trap returned non-object ('1')
复制代码

handler.defineProperty()

用于拦截 Object.defineProperty() 操作

defineProperty: function(target, property, descriptor) {}
复制代码
  • target :目标对象。
  • property :待检索其描述的属性名。
  • descriptor :待定义或修改的属性的描述符。

注意:

  • defineProperty 方法也必须返回一个布尔值,表示定义该属性的操作是否成功。(严格模式下返回 false 会抛 TypeError)
  • defineProperty 方法只能接受如下标准属性,其余的将直接无法获取(示例代码如下):
    • enumerable
    • configurable
    • writable
    • value
    • get
    • set
var p = new Proxy({}, {defineProperty(target, prop, descriptor) {console.log(descriptor);return Reflect.defineProperty(target, prop, descriptor);}
});Object.defineProperty(p, 'name', {value: 'proxy',type: 'custom'
}); 
// { value: 'proxy' }
复制代码

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

  • Object.defineProperty()
  • Reflect.defineProperty()

约束:

  • 如果目标对象不可扩展, 将不能添加属性。
    const obj = {a: 10};Object.preventExtensions(obj);const p = new Proxy(obj, {defineProperty(target, prop, descriptor) {return true;}});Object.defineProperty(p, 'name', {value: 'proxy'}); // 'defineProperty' on proxy: trap returned truish for adding property 'name'  to the non-extensible proxy target
复制代码
  • 如果属性不是作为目标对象的不可配置的属性存在,则无法将属性添加或修改为不可配置。
    const obj = {a: 10};const p = new Proxy(obj, {defineProperty(target, prop, descriptor) {return true;}});Object.defineProperty(p, 'a', {value: 'proxy',configurable: false,}); // trap returned truish for defining non-configurable property 'a' which is either non-existant or configurable in the proxy target
复制代码
  • 如果目标对象存在一个对应的可配置属性,这个属性可能不会是不可配置的。
  • 如果一个属性在目标对象中存在对应的属性,那么 Object.defineProperty(target, prop, descriptor) 将不会抛出异常。
  • 在严格模式下, false 作为 handler.defineProperty 方法的返回值的话将会抛出 TypeError 异常.
    const obj = {a: 10};const p = new Proxy(obj, {defineProperty(target, prop, descriptor) {return false}});Object.defineProperty(p, 'a', {value: 'proxy',}); // 'defineProperty' on proxy: trap returned falsish for property 'a'
复制代码

handler.deleteProperty()

用于拦截对对象属性的 delete 操作

deleteProperty: function(target, property) {}
复制代码
  • target : 目标对象。
  • property : 待删除的属性名。

返回值: 必须返回一个 Boolean 类型的值,表示了该属性是否被成功删除。(这次返回 false 不会报错了)

该方法会拦截以下操作:

  • 删除属性: delete proxy[foo]delete proxy.foo
  • Reflect.deleteProperty()

约束:

  • 如果目标对象的属性是不可配置的,那么该属性不能被删除。并且尝试删除会抛 TypeError
    const obj = {};Object.defineProperty(obj, 'a', {value: 'proxy',});const p = new Proxy(obj, {deleteProperty: function (target, prop) {return true;}});delete p.a; // trap returned truish for property 'a' which is non-configurable in the proxy target
复制代码

handler.getOwnPropertyDescriptor()

用于拦截对对象属性的 getOwnPropertyDescriptor() 方法

getOwnPropertyDescriptor: function(target, prop) {}
复制代码
  • target :目标对象。
  • prop :属性名。

返回值: 必须返回一个 objectundefined。

该方法会拦截以下操作:

  • Object.getOwnPropertyDescriptor()
  • Reflect.getOwnPropertyDescriptor()

约束:

  • getOwnPropertyDescriptor 必须返回一个 objectundefined
    const obj = { a: 10 };const p = new Proxy(obj, {getOwnPropertyDescriptor: function(target, prop) {return '';}});Object.getOwnPropertyDescriptor(p, 'a'); // trap returned neither object nor undefined for property 'a'
复制代码
  • 如果属性作为目标对象的不可配置的属性存在,则该属性无法报告为不存在。
    const obj = { a: 10 };Object.defineProperty(obj, 'b', {value: 20});const p = new Proxy(obj, {getOwnPropertyDescriptor: function(target, prop) {return undefined;}});Object.getOwnPropertyDescriptor(p, 'b'); // trap returned undefined for property 'b' which is non-configurable in the proxy target
复制代码
  • 如果属性作为目标对象的属性存在,并且目标对象不可扩展,则该属性无法报告为不存在。
    const obj = { a: 10 };Object.preventExtensions(obj);const p = new Proxy(obj, {getOwnPropertyDescriptor: function(target, prop) {return undefined;}});Object.getOwnPropertyDescriptor(p, 'a'); // trap returned undefined for property 'a' which exists in the non-extensible proxy target
复制代码
  • 如果属性不存在作为目标对象的属性,并且目标对象不可扩展,则不能将其报告为存在。
    const obj = { a: 10 };Object.preventExtensions(obj);const p = new Proxy(obj, {getOwnPropertyDescriptor: function(target, prop) {return Object.getOwnPropertyDescriptor(obj, prop) || {};}});console.log(Object.getOwnPropertyDescriptor(p, 'a'))Object.getOwnPropertyDescriptor(p, 'b'); // trap returned descriptor for property 'b' that is incompatible with the existing property in the proxy target
复制代码
  • 如果一个属性作为目标对象的自身属性存在,或者作为目标对象的可配置的属性存在,则它不能被报告为不可配置
    const obj = { a: 10 };const p = new Proxy(obj, {getOwnPropertyDescriptor: function(target, prop) {return { configurable: false };}});Object.getOwnPropertyDescriptor(p, 'a'); // trap reported non-configurability for property 'a' which is either non-existant or configurable in the proxy target
复制代码
  • Object.getOwnPropertyDescriptor(target)的结果可以使用 Object.defineProperty 应用于目标对象,也不会抛出异常。

handler.getPrototypeOf()

用于拦截读取代理对象的原型的方法

getPrototypeOf(target) {}
复制代码
  • target : 被代理的目标对象。

返回值: 必须返回一个对象值或者返回 null ,不能返回其它类型的原始值。

该方法会拦截以下操作:

  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • __proto__
  • Object.prototype.isPrototypeOf()
  • instanceof

举例如下:

const obj = {};
const p = new Proxy(obj, {getPrototypeOf(target) {return Array.prototype;}
});
console.log(Object.getPrototypeOf(p) === Array.prototype,  // trueReflect.getPrototypeOf(p) === Array.prototype, // truep.__proto__ === Array.prototype,               // trueArray.prototype.isPrototypeOf(p),              // truep instanceof Array                             // true
);
复制代码

约束:

  • getPrototypeOf() 方法返回的不是对象也不是 null
    const obj = {};const p = new Proxy(obj, {getPrototypeOf(target) {return "foo";}});Object.getPrototypeOf(p); // TypeError: trap returned neither object nor null
复制代码
  • 目标对象是不可扩展的,且 getPrototypeOf() 方法返回的原型不是目标对象本身的原型。
    const obj = {};Object.preventExtensions(obj);const p = new Proxy(obj, {getPrototypeOf(target) {return {};}});Object.getPrototypeOf(p); // proxy target is non-extensible but the trap did not return its actual prototype
复制代码

handler.has()

主要用于拦截 inwith 操作

has: function(target, prop) {}
复制代码
  • target : 目标对象
  • prop : 需要检查是否存在的属性

返回值: Boolean (返回一个可以转化为 Boolean 的也没什么问题)

该方法会拦截以下操作:

  • 属性查询: foo in proxy
  • 继承属性查询: foo in Object.create(proxy)
  • with 检查: with(proxy) { (foo); }
  • Reflect.has()

约束:

  • 如果目标对象的某一属性本身不可被配置,则该属性不能够被代理隐藏
    const obj = {};Object.defineProperty(obj, 'a', {value: 10})const p = new Proxy(obj, {has: function (target, prop) {return false;}});'a' in p; // trap returned falsish for property 'a' which exists in the proxy target as non-configurable
复制代码
  • 如果目标对象为不可扩展对象,则该对象的属性不能够被代理隐藏
    const obj = { a: 10 };Object.preventExtensions(obj);const p = new Proxy(obj, {has: function(target, prop) {return false;}});'a' in p; // trap returned falsish for property 'a' but the proxy target is not extensible
复制代码

handler.isExtensible()

用于拦截对对象的 Object.isExtensible() 操作

isExtensible: function(target) {}
复制代码
  • target : 目标对象。

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

  • Object.isExtensible()
  • Reflect.isExtensible()

返回值: Boolean 值或可转换成 Boolean 的值。

约束:

  • Object.isExtensible(proxy) 必须同 Object.isExtensible(target) 返回相同值。
    • 如果 Object.isExtensible(target) 返回 ture ,则 Object.isExtensible(proxy) 必须返回 true 或者为 true 的值
    • 如果 Object.isExtensible(target) 返回 false ,则 Object.isExtensible(proxy) 必须返回 false 或者为 false 的值
    const p = new Proxy({}, {isExtensible: function(target) {return false;}});Object.isExtensible(p); // trap result does not reflect extensibility of proxy target (which is 'true')
复制代码

handler.ownKeys()

用于拦截 Reflect.ownKeys()

ownKeys: function(target) {}
复制代码
  • target : 目标对象

返回值: 一个可枚举对象

该方法会拦截目标对象的以下操作(同时有一些额外的限制):

  • Object.getOwnPropertyNames()
    • 返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)
    • 返回的结果中只能拿到 String 的,Symbol 类型的将被忽视
  • Object.keys()
    • 返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for...in 循环遍历该对象时返回的顺序一致。 可枚举的属性可以通过 for...in 循环进行遍历(除非该属性名是一个Symbol)
    • 所以返回的结果中只能拿到可枚举的 String 数组
  • Object.getOwnPropertySymbols()
    • 只返回一个给定对象自身的所有 Symbol 属性的数组
    • 返回的结果中只能拿到 SymbolString 类型的将被忽视
  • Reflect.ownKeys()
    • 返回一个目标对象自身的属性键
    • 什么都返回
    const mySymbel = Symbol('juanni');const obj = { a: 10 };Object.defineProperty(obj, 'b', { configurable: false, enumerable: false, value: 10 });Object.defineProperty(obj, mySymbel, { configurable: true, enumerable: true, value: 10 });const p = new Proxy(obj, {ownKeys: function (target) {return ['a', 'b', mySymbel];}});console.log(Object.getOwnPropertySymbols(p)); // [Symbol(juanni)]console.log(Object.getOwnPropertyNames(p)); // ["a", "b"]console.log(Object.keys(p)); // ["a"]console.log(Reflect.ownKeys(p)); // ["a", "b", Symbol(juanni)]
复制代码

约束:

  • ownKeys 的结果必须是一个数组
    const obj = {a: 10};const p = new Proxy(obj, {ownKeys: function (target) {return 123;}});Object.getOwnPropertyNames(p); // CreateListFromArrayLike called on non-object
复制代码
  • 数组的元素类型要么是一个 String ,要么是一个 Symbol
    const obj = {a: 10};const p = new Proxy(obj, {ownKeys: function (target) {return [123];}});Object.getOwnPropertyNames(p); // 123 is not a valid property name
复制代码
  • 结果列表必须包含目标对象的所有不可配置( non-configurable )、自有( own )属性的 key
    const obj = {a: 10};Object.defineProperty(obj, 'b', { configurable: false, enumerable: true, value: 10 });const p = new Proxy(obj, {ownKeys: function (target) {return [];}});Object.getOwnPropertyNames(p); // trap result did not include 'b'
复制代码
  • 如果目标对象不可扩展,那么结果列表必须包含目标对象的所有自有( own )属性的 key ,不能有其它值
    const obj = { a: 10 };Object.preventExtensions(obj);const p = new Proxy(obj, {ownKeys: function (target) {return ['a', 'd'];}});Object.getOwnPropertyNames(p); // trap returned extra keys but proxy target is non-extensible
复制代码

handler.preventExtensions()

用于拦截对对象的 Object.preventExtensions() 操作

preventExtensions: function(target) {}
复制代码
  • target : 所要拦截的目标对象

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

  • Object.preventExtensions()
  • Reflect.preventExtensions()

返回值: Boolean

约束:

  • 只有当 Object.isExtensible(proxy)falseObject.preventExtensions(proxy) 才能 true
    const p = new Proxy({}, {preventExtensions: function (target) {return true;}});Object.preventExtensions(p); // trap returned truish but the proxy target is extensible
复制代码

handler.setPrototypeOf()

用于拦截对对象的 Object.setPrototypeOf() 操作

setPrototypeOf: function(target, prototype) {}
复制代码
  • target : 被拦截目标对象
  • prototype : 对象新原型或为 null

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

  • Object.setPrototypeOf()
  • Reflect.setPrototypeOf()

返回值: Boolean

约束:

  • 如果 target 不可扩展, 原型参数必须与 Object.getPrototypeOf(target) 的值相
    const obj = {a: 10};Object.preventExtensions(obj);const p = new Proxy(obj, {setPrototypeOf(target, prototype) {Object.setPrototypeOf(target, prototype)return true;}});Object.setPrototypeOf(obj, null); // #<Object> is not extensible
复制代码

撤销 Proxy

Proxy.revocable() 方法被用来创建可撤销的 Proxy 对象。此种代理可以通过revoke函数来撤销并且关闭代理。关闭代理后,在代理上的任意的操作都会导致 TypeError

const revocable = Proxy.revocable({}, {get: function (target, name) {return "[[" + name + "]]";}
});
const proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"revocable.revoke();console.log(proxy.foo); // Cannot perform 'get' on a proxy that has been revoked
proxy.foo = 1 // Cannot perform 'set' on a proxy that has been revoked
delete proxy.foo; // Cannot perform 'deleteProperty' on a proxy that has been revoked
typeof proxy // "object", typeof doesn't trigger any trap
复制代码

在 Mobx5 中的运用

在这里打算用 Vue 和 Mobx 来体现出 Proxy 的优势

首先看看 Vue2.x 因为使用 defineProperty 带来的限制:

  1. 不能检测利用索引直接设置一个项: vm.items[indexOfItem] = newValue
  2. 不能检测修改数组的长度: vm.items.length = newLength
  3. 不能检测对象属性的添加或删除
  4. ...

隔壁 Mobx4 也是用的 defineProperty ,但是通过一系列 hack 来绕过一些限制:

  1. 使用自己撸的类数组对象来解决上述 1、2 两个问题。但是也有额外限制:
    1. Array.isArray 返回 false
    2. 传递给外部或者需要使用真正数组时,必须使用 array.slice() 创建一份浅拷贝的真正数组
    3. sortreverse 不会改变数组本身,而只是返回一个排序过/反转过的拷贝
  2. 没能解决不能直接在对象上添加/删除属性的问题

因为使用了类数组对象,所以 length 变成了对象上的属性而不是数组的 length ,因此可以被劫持。更多技巧可以查看 observablearray.ts

    Object.defineProperty(ObservableArray.prototype, "length", {enumerable: false,configurable: true,get: function(): number {return this.$mobx.getArrayLength()},set: function(newLength: number) {this.$mobx.setArrayLength(newLength)}})
复制代码

Mobx5 在今年使用 Prxoy 重写后正式发布,成功解决了上述问题,接下来简单看一看它是如何解决的:

  1. 拦截修改数组 length 的操作/直接设置值:
const arrayTraps = {get(target, name) {if (name === $mobx) return target[$mobx]// 成功拦截 lengthif (name === "length") return target[$mobx].getArrayLength()if (typeof name === "number") {return arrayExtensions.get.call(target, name)}if (typeof name === "string" && !isNaN(name as any)) {return arrayExtensions.get.call(target, parseInt(name))}if (arrayExtensions.hasOwnProperty(name)) {return arrayExtensions[name]}return target[name]},set(target, name, value): boolean {// 成功拦截 lengthif (name === "length") {target[$mobx].setArrayLength(value)return true}// 直接设置数组值if (typeof name === "number") {arrayExtensions.set.call(target, name, value)return true}// 直接设置数组值if (!isNaN(name)) {arrayExtensions.set.call(target, parseInt(name), value)return true}return false},preventExtensions(target) {fail(`Observable arrays cannot be frozen`)return false}
}
复制代码
  1. 直接在对象上添加/删除属性
    • 这一点本质是因为 defineProperty 是劫持的对象上的属性引起的,没有办法劫持对象上不存在的属性,而 Prxoy 劫持整个对象自然没有了这个问题

polyfill

这个东西因为语言本身限制所以 polyfill 并不好搞,但是部分实现还是可以的:

proxy-polyfill 是谷歌基于 defineProperty 撸的,只支持 get , set , apply , construct, 也支持 revocable ,代码只有一百多行非常简单,所以就不多做讲解

实际运用

基础知识了解了这么多,接下来该看看实际运用了

设计模式

正好有个设计模式叫代理模式:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

优点有二:

  • 单一职责原则: 面向对象设计中鼓励将不同的职责分布到细粒度的对象中, Proxy 在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。
  • 开放-封闭原则:代理可以随时从程序中去掉,而不用对其他部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种原因不再需要代理,那么就可以容易的将代理对象换成原对象的调用

需要注意的 this

在热身之前有一个需要注意的小点 - this

    const target = {foo() {return {thisIsTarget: this === target,thisIsProxy: this === proxy,};}};const handler = {};const proxy = new Proxy(target, handler);console.log(target.foo()); // {thisIsTarget: true, thisIsProxy: false}console.log(proxy.foo()); // {thisIsTarget: false, thisIsProxy: true}
复制代码

通常情况下,通过 Proxy 中的 this 来调用方法或者获取/设置属性没什么问题,因为最终还是会被拦截到走到原始对象上,但是如果是本身用 this 进行骚操作或是有些内置方法需要 this 指向正确就需要额外注意了

  1. this 使用骚操作需要额外注意
    const _name = new WeakMap();class Person {constructor(name) {_name.set(this, name);}get name() {return _name.get(this);}}const juanni = new Person('Juanni');const proxy = new Proxy(juanni, {});console.log(juanni.name); // 'juanni'console.log(proxy.name); // undefined
复制代码
  1. 内置方法依赖 this
    const target = new Date();const handler = {};const proxy = new Proxy(target, handler);// 依赖 this 导致报错proxy.getDate(); // this is not a Date object.// 修正方案const handler = {get(target, propKey, receiver) {if (propKey === 'getDate') {return target.getDate.bind(target);}return Reflect.get(target, propKey, receiver);},};const proxy = new Proxy(new Date('2020-12-24'), handler);proxy.getDate(); // 24
复制代码

热身

首先让我们简单热身一下,看一个简单的:假设我们有一个函数tracePropAccess(obj, propKeys) ,只要设置或获得了 obj 的在 propKeys 的属性,就会被记录下来。

由于这个是简单的热身 demo,因此就直接给出使用 definePropertyProxy 完成的代码来供对比

// ES5function tracePropAccess(obj, propKeys) {const propData = Object.create(null);propKeys.forEach(function (propKey) {propData[propKey] = obj[propKey];Object.defineProperty(obj, propKey, {get: function () {console.log(`GET ${propKey}`);return propData[propKey];},set: function (value) {console.log(`SET ${propKey} = ${value}`);propData[propKey] = value;},});});return obj;}class Point {constructor(x, y) {this.x = x;this.y = y;}toString() {return `Point( ${this.x} , ${this.y} )`;}}p = tracePropAccess(new Point(7), ['x', 'y']);p.x // GET xp.x = 666 // SET x = 666p.toString()// GET x// GET y
复制代码
// ES6 with Proxyfunction tracePropAccess(obj, propKeys) {const propKeySet = new Set(propKeys);return new Proxy(obj, {get(target, propKey, receiver) {if (propKeySet.has(propKey)) {console.log(`GET ${propKey}`);}return Reflect.get(target, propKey, receiver);},set(target, propKey, value, receiver) {if (propKeySet.has(propKey)) {console.log(`SET ${propKey} = ${value}`);}return Reflect.set(target, propKey, value, receiver);},});}class Point {constructor(x, y) {this.x = x;this.y = y;}toString() {return `Point( ${this.x} , ${this.y} )`;}}p = tracePropAccess(new Point(7), ['x', 'y']);p.x // GET xp.x = 666 // SET x = 666p.toString()// GET x// GET y
复制代码

负数组索引

隔壁 python 等都可以通过负数索引访问到数组倒数第 N 个元素,现在我们有了一种新方法直接实现这一特性:

    function createArray(array) {if(!Array.isArray(array)) {throw Error('must be an array');}const handler = {get(target, propKey, receiver) {const index = Number(propKey);if (index < 0) {propKey = String(target.length + index);}return Reflect.get(target, propKey, receiver);}};return new Proxy(array, handler);}const arr = createArray(['a', 'b', 'c']);console.log(arr[-1]); // c
复制代码

拦截调用

对于方法调用没有单一操作可以进行拦截,因为方法调用被视为两个独立的操作:首先使用 get 检索函数,然后调用该函数。

const obj = {multiply(x, y) {return x * y;},squared(x) {return this.multiply(x, x);},
};function traceMethodCalls(obj) {const handler = {get(target, propKey, receiver) {const origMethod = target[propKey];return function (...args) {const result = origMethod.apply(this, args);console.log(propKey + JSON.stringify(args)+ ' -> ' + JSON.stringify(result));return result;};}};return new Proxy(obj, handler);
}
const tracedObj = traceMethodCalls(obj);console.log(tracedObj.multiply(2,7));
// multiply[2,7] -> 14
// test.js:25 14
console.log(tracedObj.squared(9));
// multiply[9,9] -> 81
// test.js:16 squared[9] -> 81
// test.js:26 81
复制代码

我们可以看见即使 this 指向了 Proxy 在原始对象内部的方法调用(如 this.multiply(x, x) )也能被拦截到

单例模式

function singleton(func) {let instance,handler = {construct: function (target, args) {if (!instance) {instance = new func();}return instance;}};return new Proxy(func, handler);
}
复制代码

让某个属性彻底消失

  • Reflect.hasObject.hasOwnPropertyObject.prototype.hasOwnPropertyin 运算符全部使用了 [[HasProperty]],可以通过 has 拦截。
  • Object.keysObject.getOwnPropertyNames , Object.entrie 都使用了 [[OwnPropertyKeys]],可以通过 ownKeys 拦截。
  • Object.getOwnPropertyDescriptor 使用了 [[GetOwnProperty]]可以通过 getOwnPropertyDescriptor 拦截。

因此我们可以写出如下代码彻底让某个属性彻底消失掉

function hideProperty(object, ...propertiesToHide) {const proxyObject = new Proxy(object, {has(object, property) {if (propertiesToHide.indexOf(property) != -1) {return false;}return Reflect.has(object, property);},ownKeys(object) {return Reflect.ownKeys(object).filter((property) => propertiesToHide.indexOf(property) == -1);},getOwnPropertyDescriptor(object, property) {if (propertiesToHide.indexOf(property) != -1) {return undefined;}return Reflect.getOwnPropertyDescriptor(object, property);}});return proxyObject;
}
复制代码

其他

这里还有很多可以用 Proxy 来实现的,比如:

  • 类型校验
    • 剥离校验逻辑
  • 私有变量
    • 通过使用 has, ownKeys , getOwnPropertyDescriptorget, set 来让属性变成私有属性
  • 数据绑定
  • 缓存代理
  • 验证代理
  • 图片懒加载
  • 合并请求
  • 更多有趣的作品可以查看 awesome-es2015-proxy

参考

  • Proxy
  • [译]ES6 中的元编程:第二部分 —— 反射(Reflect)
  • [译]ES6 中的元编程: 第三部分 —— 代理(Proxies)
  • ECMAScript® 2015 Language Specification
  • Metaprogramming with proxies
  • 抱歉,学会 Proxy 真的可以为所欲为


作者:眷你
链接:https://juejin.im/post/5c170b556fb9a049ee805bdc
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这篇关于Proxy详解,运用与Mobx的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/509163

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

poj 2431 poj 3253 优先队列的运用

poj 2431: 题意: 一条路起点为0, 终点为l。 卡车初始时在0点,并且有p升油,假设油箱无限大。 给n个加油站,每个加油站距离终点 l 距离为 x[i],可以加的油量为fuel[i]。 问最少加几次油可以到达终点,若不能到达,输出-1。 解析: 《挑战程序设计竞赛》: “在卡车开往终点的途中,只有在加油站才可以加油。但是,如果认为“在到达加油站i时,就获得了一

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP

嵌入式Openharmony系统构建与启动详解

大家好,今天主要给大家分享一下,如何构建Openharmony子系统以及系统的启动过程分解。 第一:OpenHarmony系统构建      首先熟悉一下,构建系统是一种自动化处理工具的集合,通过将源代码文件进行一系列处理,最终生成和用户可以使用的目标文件。这里的目标文件包括静态链接库文件、动态链接库文件、可执行文件、脚本文件、配置文件等。      我们在编写hellowor

LabVIEW FIFO详解

在LabVIEW的FPGA开发中,FIFO(先入先出队列)是常用的数据传输机制。通过配置FIFO的属性,工程师可以在FPGA和主机之间,或不同FPGA VIs之间进行高效的数据传输。根据具体需求,FIFO有多种类型与实现方式,包括目标范围内FIFO(Target-Scoped)、DMA FIFO以及点对点流(Peer-to-Peer)。 FIFO类型 **目标范围FIFO(Target-Sc

019、JOptionPane类的常用静态方法详解

目录 JOptionPane类的常用静态方法详解 1. showInputDialog()方法 1.1基本用法 1.2带有默认值的输入框 1.3带有选项的输入对话框 1.4自定义图标的输入对话框 2. showConfirmDialog()方法 2.1基本用法 2.2自定义按钮和图标 2.3带有自定义组件的确认对话框 3. showMessageDialog()方法 3.1

脏页的标记方式详解

脏页的标记方式 一、引言 在数据库系统中,脏页是指那些被修改过但还未写入磁盘的数据页。为了有效地管理这些脏页并确保数据的一致性,数据库需要对脏页进行标记。了解脏页的标记方式对于理解数据库的内部工作机制和优化性能至关重要。 二、脏页产生的过程 当数据库中的数据被修改时,这些修改首先会在内存中的缓冲池(Buffer Pool)中进行。例如,执行一条 UPDATE 语句修改了某一行数据,对应的缓

OmniGlue论文详解(特征匹配)

OmniGlue论文详解(特征匹配) 摘要1. 引言2. 相关工作2.1. 广义局部特征匹配2.2. 稀疏可学习匹配2.3. 半稠密可学习匹配2.4. 与其他图像表示匹配 3. OmniGlue3.1. 模型概述3.2. OmniGlue 细节3.2.1. 特征提取3.2.2. 利用DINOv2构建图形。3.2.3. 信息传播与新的指导3.2.4. 匹配层和损失函数3.2.5. 与Super