This paper briefly expounds a little metaprogramming knowledge, and then gives a more detailed Proxy usage (at least more detailed than MDN, supplemented with specific examples of various error situations, and more accurate than the above machine), and then uses some examples to describe Proxy suitable for what scenarios to use

metaprogramming

First, there are two levels of programming:

  • Base level/application level: code handles user input
  • yuanLevel: Code that deals with the base level of code

The word meta here means: something about something itself, so the word metaprogramming means programming about programming. Metaprogramming can be done in two of the same languages. The languages in which metaprogramming is written are called metalanguages. The language of the program being manipulated is called the “target language”, where JavaScript is the meta-language and Java is the target language:

const str = 'Hello' + '! '.repeat(3);
console.log('System.out.println("'+str+'"');
Copy the code

The ability of a programming language to be its own meta-language is called Reflection and is used to discover and tweak the structure and semantics of your application.

Metaprogramming comes in three forms:

  • Introspection: Reveals the state of the program at runtime, capturing the structure of the program in read-only form
    • Such as the useObject.keys(obj)Wait, there’s a new one on the ES6ReflectMany interfaces for capturing internal state are consolidated and unified
  • Self-modification: Modifies the program structure/data at run time
    • Such asdeleteproperty descriptors
  • Intercession: Allows for redefining the semantics of certain language operations
    • For example, in this articleProxy

Metaprogramming and Reflect

ES6 also adds a new global object named Reflect, most of which already exist in other forms, to unify its interface:

  1. In the past, the various methods of obtaining/modifying the runtime state of a program were usually scattered or hungObject.prototypeOn, some onFunction.prototypeOn, there is an operator (e.gdelete / inEtc.)
  2. Previous calls were too complex or insecure
    • Call it down in some casesobj.hasOwnPropertyThe method may not exist on the object (for example, the object is passedObject.create(null)Created), so used at this timeObject.prototype.hasOwnProperty.callIt’s the safest, but it’s too complicated
    • callapplyThere are also the above problems
  3. Return value is not scientific, as usedObject.defineProperty, returns an object on success, otherwise throws oneTypeErrorTherefore, it has to be usedtry... catchTo catch any errors that occur while defining properties. whileReflect.definePropertyReturns a Boolean success status, so you can use onlyif... else

Check out the article here to see what Reflect has done

Basic content of Proxy

Finally, let’s look at the Proxy constructor:

Proxy(target, handler)
Copy the code
  • target:ProxyThe wrapped target object (which can be any type of object, including a native array, a function, or even another proxy).
  • handler: processor object (proxy's handler) with a variety of proxyable operations from the defined proxy object. Among them are numeroustraps

Invariant (Invariants)

Before delving further into what proxies do, let’s review how to pass protected objects:

  • Non-extensible
    • Cannot add attributes and cannot change stereotypes
    'use strict'
    const obj = Object.preventExtensions({});
    console.log(Object.isExtensible(obj)); // false
    obj.foo = 123; // Cannot add property foo, object is not extensible
    Object.setPrototypeOf(obj, null) // #<Object> is not extensible
Copy the code
  • Do not write (Non – writable)
    • valueCannot be changed by the assignment operator
  • Non-configurable (non-64x)
    • Properties cannot be changed/deleted (except for thewritableInstead offalse

Using a Proxy, it is easy to violate the above constraints (because these constraints apply to Proxy objects, but Proxy objects are not bound by them). So the Proxy checks for us on calls/returns, casts truish and falsish to Boolean when we expect Boolean, etc. Further explanations and examples are provided in the constraints section below.

Here’s a document on invariants

Then let’s look at what the Proxy handler provides for us to use

handler.get()

Intercepts read property operations on objects.

get: function(target, property, receiver) {}
Copy the code
  • target: Target object.
  • property: Name of the property to be obtained.
  • receiver: The object that was called initially. Is usuallyproxyItself, buthandlergetMethods can also be invoked indirectly on the prototype chain or in other ways (so not necessarilyproxyItself).
    • Here,targetpropertyIt’s all perfectly understandable, butreceiverFor extra attention, here’s an example to help you understand:
    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}
    // You can see that receiver is the object that is called initially
Copy the code

This method intercepts the following operations on the target object:

  • Access properties:proxy[foo]proxy.bar
  • Access properties on the prototype chain:Object.create(proxy)[foo]
  • Reflect.get()

Constraint (violation of the constraint raises a Type Error) :

  • If the target property to be accessed is not writable and configurable, the value returned must be the same as that of the target property.
    const obj = {};
    // Not writable and not configurable
    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')
Copy the code
  • If the properties of the target object are not configurable and are not definedgetMethod, its return value must beundefined
    const obj = { a: 10 };
    // Cannot be configured and get is not defined
    Object.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')
Copy the code

handler.set()

Intercepts an operation to set a property value

set: function(target, property, value, receiver) {}
Copy the code
  • target: Target object.
  • property: Specifies the name of the property to be set.
  • value: New value to be set
  • receiver: The object that was called initially. With the abovegetIn thereceiver

The return value:

The set method should return a Boolean:

  • returntrueThis indicates that setting the property succeeded
  • returnfalseIf the set property action occurs in strict mode, one is thrownTypeError

Note: Most methods in Proyx will essentially convert the return value to Boolean, so you can return whatever you want inside and get a Boolean outside; That’s why the words “truish” and “falsish” are used in errors

This method intercepts the following operations on the target object:

  • Specify attribute values:proxy[foo] = barproxy.foo = bar
  • Specify the inheritor’s property value:Object.create(proxy)[foo] = bar
  • Reflect.set()

Constraints:

  • If the target property is not writable and configurable, its value cannot be changed.
    const obj = {};
    // Not writable and not configurable
    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
    // Note that we are not really changing the value of 'a', this error is caused by return true
Copy the code
  • If the properties of the target object are not configurable and are not definedsetMethod, cannot set its value.
    const obj = {};
    // Unwritable and set not defined
    Object.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
    // Note that we are not really changing the value of 'a', this error is caused by return true
Copy the code
  • In strict mode, ifsetMethod returnsfalse, will throw oneTypeErrorThe exception.
    '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'
Copy the code

handler.apply()

Intercepting function calls

apply: function(target, thisArg, argumentsList) {}
Copy the code
  • target: Target object (function).
  • thisArg: The context object when called.
  • argumentsList: The array of arguments when called.

This method intercepts the following operations on the target object:

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

Constraints:

  • targetMust itself be callable. That is, it must be a function object.

handler.construct()

Used to intercept the new operator

construct: function(target, argumentsList, newTarget) {}
Copy the code
  • target: Target object.
  • argumentsList :constructorParameter list.
  • newTarget: The constructor that was originally called.

This method intercepts the following operations on the target object:

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

Note:

  • In order to make thenewOperator in the generatedProxyObject, the target object used to initialize the proxy must itself have[[Construct]]Internal methods, i.enew targetIt has to be valid. For instancetargetIs afunction

Constraints:

  • constructMethod must return an object or an error will be thrownTypeError
    const p = new Proxy(function () {}, {
        construct: function (target, argumentsList, newTarget) {
            return 1; }});new p(); // 'construct' on proxy: trap returned non-object ('1')
Copy the code

handler.defineProperty()

Used to intercept the Object.defineProperty() operation

defineProperty: function(target, property, descriptor) {}
Copy the code
  • target: Target object.
  • property: The name of the attribute whose description is to be retrieved.
  • descriptor: Descriptor of the property to be defined or modified.

Note:

  • definePropertyThe method must also return a Boolean value indicating whether the operation defining the property was successful. Return in strict modefalseFlip aTypeError)
  • definePropertyThe method can only accept the following standard attributes, and the rest are not directly available (example code below) :
    • 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' }
Copy the code

This method intercepts the following operations on the target object:

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

Constraints:

  • If the target object is not extensible, attributes cannot be added.
    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
Copy the code
  • You cannot add or modify a property to be unconfigurable if it does not exist as a non-configurable property of the target object.
    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
Copy the code
  • If the target object has a corresponding configurable property, that property may not be unconfigurable.
  • If an attribute has a corresponding attribute in the target object, thenObject.defineProperty(target, prop, descriptor)No exception will be thrown.
  • In strict mode,falseAs ahandler.definePropertyMethod return value will be thrownTypeErrorThe exception.
    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'
Copy the code

handler.deleteProperty()

Used to intercept delete operations on object properties

deleteProperty: function(target, property) {}
Copy the code
  • target: Target object.
  • property: Indicates the name of the attribute to be deleted.

Return value: A Boolean value must be returned indicating whether the property was successfully deleted. (This time return false will not error)

This method intercepts the following operations:

  • Delete attributes:delete proxy[foo]delete proxy.foo
  • Reflect.deleteProperty()

Constraints:

  • If a property of the target object is not configurable, it cannot be deleted. And trying to delete will throwTypeError
    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
Copy the code

handler.getOwnPropertyDescriptor()

Used to intercept the getOwnPropertyDescriptor() method on object properties

getOwnPropertyDescriptor: function(target, prop) {}
Copy the code
  • target: Target object.
  • prop: Attribute name.

Return value: An object or undefined must be returned.

This method intercepts the following operations:

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

Constraints:

  • getOwnPropertyDescriptorMust return oneobjectundefined
    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'
Copy the code
  • If a property exists as a non-configurable property of the target object, the property cannot be reported as nonexistent.
    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
Copy the code
  • If a property exists as a property of the target object and the target object is not extensible, the property cannot be reported as nonexistent.
    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
Copy the code
  • If an attribute does not exist as a target object, and the target object is not extensible, it cannot be reported as existing.
    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
Copy the code
  • If a property exists as a property of the target object itself, or as a configurable property of the target object, it cannot be reported as unconfigurable
    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
Copy the code
  • Object.getOwnPropertyDescriptor(target)The results can be usedObject.definePropertyApplies to the target object and does not throw an exception.

handler.getPrototypeOf()

A method used to intercept a stereotype that reads a proxy object

getPrototypeOf(target) {}
Copy the code
  • target: Target object to be proxied.

Return value: must return an object value or null, cannot return a primitive value of another type.

This method intercepts the following operations:

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

Examples are as follows:

const obj = {};
const 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
);
Copy the code

Constraints:

  • getPrototypeOf()Method returns neither an object nor an objectnull
    const obj = {};
    const p = new Proxy(obj, {
        getPrototypeOf(target) {
            return "foo"; }});Object.getPrototypeOf(p); // TypeError: trap returned neither object nor null
Copy the code
  • The target object is not extensible, andgetPrototypeOf()The prototype returned by the method is not the prototype of the target object itself.
    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
Copy the code

handler.has()

Mainly used to intercept in and with operations

has: function(target, prop) {}
Copy the code
  • target: Target object
  • prop: Attributes that need to be checked for existence

Return value: Boolean (There is nothing wrong with returning a Boolean that can be converted to Boolean)

This method intercepts the following operations:

  • Attribute query:foo in proxy
  • Inherited attribute query:foo in Object.create(proxy)
  • withCheck:with(proxy) { (foo); }
  • Reflect.has()

Constraints:

  • If a property of the target object itself cannot be configured, the property cannot be hidden by the proxy
    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
Copy the code
  • If the target object is an unextensible object, the properties of that object cannot be hidden by the proxy
    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
Copy the code

handler.isExtensible()

Used to intercept the Object.isextensible () operation on an Object

isExtensible: function(target) {}
Copy the code
  • target: Target object.

This method intercepts the following operations on the target object:

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

Return value: Boolean value or a value that can be converted to Boolean.

Constraints:

  • Object.isExtensible(proxy)Must be the sameObject.isExtensible(target)Returns the same value.
    • ifObject.isExtensible(target)returnture,Object.isExtensible(proxy)Must be returnedtrueOr fortrueThe value of the
    • ifObject.isExtensible(target)returnfalse,Object.isExtensible(proxy)Must be returnedfalseOr forfalseThe value of the
    const p = new Proxy({}, {
        isExtensible: function(target) {
            return false; }});Object.isExtensible(p); // trap result does not reflect extensibility of proxy target (which is 'true')
Copy the code

handler.ownKeys()

Used to intercept reflect.ownkeys ()

ownKeys: function(target) {}
Copy the code
  • target: Target object

Return value: an enumerable object

This method intercepts the following operations on the target object (with some additional restrictions) :

  • Object.getOwnPropertyNames()
    • Returns the property name of all the properties of the specified object (including non-enumerable properties but not those with the Symbol value as the name)
    • You can only get the results that are returnedStringThe,SymbolType will be ignored
  • Object.keys()
    • Returns an array of the self-enumerable properties of a given object, the order and use of the property names in the arrayfor... inThe same order is returned as the loop iterates through the object. Enumerable properties can be passedfor... inLoop through (unless the property name is a Symbol)
    • So you can only get enumerable results backStringAn array of
  • Object.getOwnPropertySymbols()
    • Returns only an array of all Symbol attributes for a given object itself
    • You can only get the results that are returnedSymbolStringType will be ignored
  • Reflect.ownKeys()
    • Returns the property key of the target object itself
    • Return everything
    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)]
Copy the code

Constraints:

  • ownKeysMust result in an array
    const obj = {
        a: 10
    };
    const p = new Proxy(obj, {
        ownKeys: function (target) {
            return 123; }});Object.getOwnPropertyNames(p); // CreateListFromArrayLike called on non-object
Copy the code
  • The element type of the array is either oneStringOr oneSymbol
    const obj = {
        a: 10
    };
    const p = new Proxy(obj, {
        ownKeys: function (target) {
            return [123]; }});Object.getOwnPropertyNames(p); // 123 is not a valid property name
Copy the code
  • The result list must contain all non-configurable (non-configurable), own (own) properties ofkey
    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'
Copy the code
  • If the target object is not extensible, the result list must contain all of the target object’s own (own) properties ofkeyCan’t have any other value
    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
Copy the code

handler.preventExtensions()

Used to intercept the Object.preventExtensions() operation on the Object

preventExtensions: function(target) {}
Copy the code
  • target: The target object to intercept

This method intercepts the following operations on the target object:

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

Return value: Boolean

Constraints:

  • Only when theObject.isExtensible(proxy)falseObject.preventExtensions(proxy)In order totrue
    const p = new Proxy({}, {
        preventExtensions: function (target) {
            return true; }});Object.preventExtensions(p); // trap returned truish but the proxy target is extensible
Copy the code

handler.setPrototypeOf()

Used to intercept object.setPrototypeof () operations on objects

setPrototypeOf: function(target, prototype) {}
Copy the code
  • target: Intercepted target object
  • prototype: Object new prototype or fornull

This method intercepts the following operations on the target object:

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

Return value: Boolean

Constraints:

  • iftargetUnextensible, prototype parameters must be matched withObject.getPrototypeOf(target)The value of the phase
    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
Copy the code

undoProxy

The proxy.revocable () method is used to create a revocable Proxy object. Such proxies can be revoked and closed by the REVOKE function. After the agent is closed, any operation on the agent causes TypeError

const revocable = Proxy.revocable({}, {
    get: function (target, name) {
        return "The [[" + 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
Copy the code

Application in Mobx5

Here we intend to use Vue and Mobx to reflect the advantages of Proxy

Let’s start with vue2.x because of the restrictions imposed by using defineProperty:

  1. Cannot detect using index to set an item directly:vm.items[indexOfItem] = newValue
  2. Cannot detect the length of the modified array:vm.items.length = newLength
  3. Cannot detect addition or deletion of object attributes
  4. .

Next door Mobx4 also uses defineProperty, but uses a series of hacks to get around some restrictions:

  1. useClass array object by itselfTo solve the above problems 1 and 2. But there are additional restrictions:
    1. Array.isArrayreturnfalse
    2. Must be used when passed outside or when you need to use a real arrayarray.slice()Create a shallow copy of the real array
    3. sortreverseDoes not change the array itself, but simply returns a sorted/reversed copy
  2. Failed to resolve the issue of not being able to add/remove attributes directly on objects

Because an array-like object is used, length becomes an attribute on the object rather than the length of the array, and can therefore be hijacked. For more tips, check out 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)
        }
    })
Copy the code

Mobx5, which was released this year using a Prxoy rewrite, successfully addresses these issues. Here’s a quick look at how it works:

  1. Intercepting the operation to modify array length/directly set the value:
const arrayTraps = {
    get(target, name) {
        if (name === $mobx) return target[$mobx]
        // Successfully intercepted length
        if (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 {
        // Successfully intercepted length
        if (name === "length") {
            target[$mobx].setArrayLength(value)
            return true
        }
        // Set the array value directly
        if (typeof name === "number") {
            arrayExtensions.set.call(target, name, value)
            return true
        }
        // Set the array value directly
        if (!isNaN(name)) {
            arrayExtensions.set.call(target, parseInt(name), value)
            return true
        }
        return false
    },
    preventExtensions(target) {
        fail(`Observable arrays cannot be frozen`)
        return false}}Copy the code
  1. Add/remove attributes directly to an object
    • This is essentially becausedefinePropertyIs caused by a property on a hijacked object, there is no way to hijack a property that does not exist on an object, andPrxoyHijacking the entire object eliminates this problem

polyfill

Polyfill isn’t easy because of language limitations, but some implementations are ok:

Proxy-polyfill is a Google DefineProperty-based app that supports only Get, set, apply, construct and Revocable. The code is only 100 lines long and very simple, so I won’t go into details

The practical application

Now that we know the basics, it’s time to put them into practice

Design patterns

There happens to be a design pattern called the proxy pattern: provide a proxy for other objects to control access to that object. In some cases, an object is inappropriate or cannot directly reference another object, and a proxy object can act as an intermediary between the client and the target object.

There are two advantages:

  • Single responsibility principle: Object-oriented design encourages the distribution of different responsibilities into fine-grained objects,ProxyBased on the original object, the function is derived without affecting the original object, which conforms to the design concept of loose coupling and high cohesion.
  • Open-closed principle: Proxies can be removed from the program at any time without changing other parts of the code. In a real world scenario, proxies may no longer be needed for a variety of reasons as the version iterations, so proxy objects can be easily replaced with calls to the original object

Be aware ofthis

There is one small point to note before warming up – 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}
Copy the code

Normally, using this in Proxy to call a method or get/set a property is fine, because it will eventually be intercepted on the original object, but if you use this or some built-in method that requires this to point correctly, you need to be careful. Okay

  1. rightthisUsing the SAO operation requires extra care
    const _name = new WeakMap(a);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
Copy the code
  1. Built-in method dependenciesthis
    const target = new Date(a);const handler = {};
    const proxy = new Proxy(target, handler);

    // An error is reported depending on this
    proxy.getDate(); // this is not a Date object.

    // Modify the scheme
    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
Copy the code

Warm up

Let’s warm up a bit and look at a simple one: suppose we have a function tracePropAccess(obj, propKeys). Whenever we set or obtain obj’s property in the propKeys, it will be logged.

Since this is a simple warm-up demo, I’ll go straight to the code done using defineProperty and Proxy for comparison

// ES5
    function 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( The ${this.x} , The ${this.y}) `;
        }
    }
    p = tracePropAccess(new Point(7),'x'.'y']);
    p.x // GET x
    p.x = Awesome! // SET x = 666
    p.toString()
    // GET x
    // GET y
Copy the code
// ES6 with Proxy
    function 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( The ${this.x} , The ${this.y}) `;
        }
    }
    p = tracePropAccess(new Point(7),'x'.'y']);
    p.x // GET x
    p.x = Awesome! // SET x = 666
    p.toString()
    // GET x
    // GET y
Copy the code

Negative group index

The NTH element of an array can be accessed using a negative index. Now we have a new way to implement this feature directly:

    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
Copy the code

Intercept calls

No single operation can be intercepted on a method call, because the method call is treated as two separate operations: the get retrieval function is used first, and then the function is called.

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));
                returnresult; }; }};return new Proxy(obj, handler);
}
const tracedObj = traceMethodCalls(obj);

console.log(tracedObj.multiply(2.7));
/ / multiply - > 14 [2, 7]
// test.js:25 14
console.log(tracedObj.squared(9));
/ / multiply [9, 9] - > 81
// test.js:16 squared[9] -> 81
// test.js:26 81
Copy the code

We can see that method calls (such as this.multiply(x, x)) can be intercepted even if this points to the Proxy inside the original object

The singleton pattern

function singleton(func) {
    let instance,
        handler = {
            construct: function (target, args) {
                if(! instance) { instance =new func();
                }
                returninstance; }};return new Proxy(func, handler);
}
Copy the code

Make a property disappear completely

  • Reflect.hasObject.hasOwnPropertyObject.prototype.hasOwnPropertyinAll operators are used[[HasProperty]], can be accessed throughhasIntercept.
  • Object.keysObject.getOwnPropertyNames , Object.entrieuse[[OwnPropertyKeys]], can be accessed throughownKeysIntercept.
  • Object.getOwnPropertyDescriptorUsing the[[GetOwnProperty]]Can be achieved bygetOwnPropertyDescriptorIntercept.

So we can write code like this to make a property disappear completely

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;
}
Copy the code

other

There are many other things you can do with a Proxy, such as:

  • Type checking
    • Peel check logic
  • Private variables
    • Through the use ofhas.ownKeys , getOwnPropertyDescriptorget.setTo make the property private
  • Data binding
  • The caching proxy
  • Verify the agent
  • Lazy loading of images
  • Merge request
  • For more interesting work, see awese-ES2015-Proxy

reference

  • Proxy
  • Metaprogramming in ES6: Part 2 — Reflect
  • Metaprogramming in ES6: Proxies
  • ECMAScript ® 2015 Language Specification
  • Metaprogramming with proxies
  • Sorry, Proxy learning can really do whatever it wants