ES6--13 object's Proxy interception -- Proxy constructor

Contents of this section

Two parameters of the Proxy constructor

Proxy is used to modify the default behavior of some operations, which is equivalent to making changes at the language level, so it belongs to a "meta programming", that is, programming the programming language.

Proxy can be understood as setting up a layer of "interception" before the target object, through which the external access to the object must first be intercepted, so it provides a mechanism to filter and rewrite the external access. The original meaning of the word proxy is proxy. It is used here to indicate that it is used to "proxy" certain operations, which can be translated as "proxy".

var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
});

The above code sets up a layer of interception for an empty object, redefining the read (get) and set (set) behaviors of properties. For the time being, we will not explain the specific syntax, but only see the running results. For the object obj with intercepting behavior set, read and write its properties, and you will get the following results.

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

The above code shows that Proxy actually overload s the dot operator, that is, it overwrites the original definition of the language with its own definition.

ES6 natively provides a Proxy constructor to generate Proxy instances.

var proxy = new Proxy(target, handler);

All uses of Proxy objects are in the above form. The only difference is the way to write the handler parameter. Among them, new Proxy() represents generating a Proxy instance, target parameter represents the target object to be intercepted, and handler parameter is also an object to customize the intercepting behavior.

Here is another example of intercepting the read property behavior.

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

In the above code, as a constructor, Proxy takes two parameters. The first parameter is the target object to be proxied (the above example is an empty object), that is, if there is no Proxy intervention, the original object to be accessed by the operation is the object; the second parameter is a configuration object, for each operation to be proxied, a corresponding processing function needs to be provided, which will intercept the corresponding operation. For example, in the above code, the configuration object has a get method to intercept access requests to the properties of the target object. The two parameters of the get method are the target object and the property to be accessed. As you can see, since the interceptor always returns 35, accessing any property gets 35.

Note that for proxy to work, you must operate on the proxy instance (the example above is a proxy object), not the target object (the example above is an empty object).

If the handler does not set any interception, it is equivalent to going directly to the original object.

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

In the above code, handler is an empty object, without any interception effect, accessing proxy is equivalent to accessing target.

One trick is to set the Proxy object to the object.proxy property, so that it can be called on the object object.

var object = { proxy: new Proxy(target, handler) };

Proxy instances can also be prototype objects for other objects.

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

In the above code, the proxy object is the prototype of the obj object, and the obj object itself does not have the time attribute, so according to the prototype chain, the attribute will be read on the proxy object, resulting in being blocked.

Method of Proxy instance

The following is a list of 13 interception operations supported by Proxy.

  • get(target, propKey, receiver): intercept the reading of object properties, such as proxy.foo and proxy['foo '].
  • set(target, propKey, value, receiver): intercepts the setting of object properties, such as proxy.foo = v or proxy['foo'] = v, and returns a Boolean value.
  • has(target, propKey): intercepts the operation of propKey in proxy and returns a Boolean value.
  • deleteProperty(target, propKey): intercepts the operation of delete proxy[propKey], and returns a Boolean value.
  • ownKeys(target): intercepts the loop of Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy), for...in, and returns an array. This method returns the property names of all the properties of the target object itself, while the return result of Object.keys() only includes the traversable properties of the target object itself.
  • getOwnPropertyDescriptor(target, propKey): intercepts the Object.getOwnPropertyDescriptor(proxy, propKey), and returns the description object of the property.
  • defineProperty(target, propKey, propDesc): intercepts Object.defineProperty(proxy, propKey, propDesc), Object.defineProperties(proxy, propDescs), and returns a Boolean value.
  • preventExtensions(target): intercepts Object.preventExtensions(proxy) and returns a Boolean value.
  • getPrototypeOf(target): intercepts Object.getPrototypeOf(proxy) and returns an object.
  • isExtensible(target): intercepts Object.isExtensible(proxy) and returns a Boolean value.
  • setPrototypeOf(target, proto): intercepts Object.setPrototypeOf(proxy, proto), and returns a Boolean value. If the target object is a function, there are two additional operations that can be intercepted.
  • apply(target, object, args): intercept the operation of Proxy instance as function call, such as proxy(...args), Proxy.call (object,... Args), proxy.apply(...).
  • construct(target, args): block the operation called by the Proxy instance as a constructor, such as new proxy(...args).

The following is a detailed introduction of the above interception methods.

Property read block - get()

The get method is used to intercept the read operation of a property. It can accept three parameters, namely, the target object, the property name and the proxy instance itself (strictly speaking, the object for which the operation is performed). The last parameter is optional.

For the usage of get method, there is already an example above, and the following is another example of blocking read operation.

var person = {
  name: "Zhang San"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
    }
  }
});

proxy.name // "Zhang three"
proxy.age // Throw an error

The above code indicates that if you access a property that does not exist in the target object, an error will be thrown. If there is no interceptor function, access to non-existent properties will only return undefined.

The get method can inherit.

let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"

In the above code, the interception operation is defined on the Prototype object, so the interception will take effect if the properties inherited by the obj object are read.

The following example uses get interception to implement array reading negative index.

function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c

In the above code, if the position parameter of the array is - 1, the last member of the array will be output.

The following is an example of the third parameter of a get method, which always points to the object where the original read operation is located. In general, it is a Proxy instance.

const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true

In the above code, the getReceiver property of the proxy object is provided by the proxy object, so the receiver points to the proxy object.

const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});

const d = Object.create(proxy);
d.a === d // true

In the above code, the d object itself has no a attribute, so when reading d.a, it will go to the prototype proxy object of d. At this point, the receiver points to d, which represents the object where the original read operation is located.

Property assignment interception - set()

The set method is used to intercept the assignment operation of a property. It can accept four parameters: target object, property name, property value and Proxy instance. The last parameter is optional.

Assuming that the Person object has an age attribute, which should be an integer no more than 200, you can use Proxy to ensure that the property value of age meets the requirements.

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // For age properties and other properties that meet the conditions, save them directly
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // Report errors
person.age = 300 // Report errors

In the above code, because the set stored value function is set, any assignment of age property that does not meet the requirements will throw an error, which is an implementation method of data validation. With the set method, you can also bind data, that is, whenever the object changes, the DOM will be updated automatically.

Sometimes, we set internal properties on objects. The first character of the property name begins with an underscore, indicating that these properties should not be used externally. By combining the get and set methods, we can prevent these internal attributes from being read or written externally.

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

In the above code, as long as the first character of the read-write property name is underline, it will be thrown wrongly, so as to achieve the purpose of prohibiting reading and writing internal properties.

The following is an example of the fourth parameter of the set method.

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true

In the above code, the fourth parameter receiver of the set method refers to the object where the original operation behavior is located. In general, it is the proxy instance itself. See the following example.

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = 'bar';
myObj.foo === myObj // true

In the above code, when setting the value of myObj.foo property, myObj has no foo property, so the engine will go to the prototype chain of myObj to find the foo property. The prototype object Proxy of myObj is a Proxy instance. Setting its foo property will trigger the set method. At this time, the fourth parameter receiver points to the object myObj where the original assignment behavior is located.

Note that if a property of the target object itself is not writable and configurable, the set method will not work.

const obj = {};
Object.defineProperty(obj, 'foo', {
  value: 'bar',
  writable: false,
});

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = 'baz';
  }
};

const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"

In the above code, the obj.foo property is not writable, and the Proxy will not take effect on the set Proxy of this property.

Function call interception - apply()

The apply method intercepts function calls, calls, and apply operations.

The apply method can accept three parameters: the target object, the context object (this) of the target object and the parameter array of the target object.

Here is an example.

var target = function () { return 'I am the target'; };
var handler = {
  apply: function () {
    return 'I am the proxy';
  }
};

var p = new Proxy(target, handler);

p()
// "I am the proxy"

In the above code, the variable p is an instance of Proxy. When it is called as a function (p()), it will be intercepted by the apply method and a string will be returned.

Here is another example.

var twice = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments) * 2;
  }
};
function sum (left, right) {
  return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30

In the above code, whenever the proxy function is executed (direct call or call and apply call), it will be intercepted by the apply method.

In addition, a direct call to the Reflect.apply method will also be intercepted.

Reflect.apply(proxy, null, [9, 10]) // 38

Function query interception - has()

The has method is used to intercept the HasProperty operation, that is, it will take effect when judging whether an object has a property. A typical operation is the in operator.

The has method can accept two parameters: the target object and the attribute name to be queried.

The following example uses the has method to hide some properties from being found by the in operator.

var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false

In the above code, if the first character of the property name of the original object is underscore, proxy.has will return false, so it will not be found by the in operator.

In addition, although the for...in loop also uses the in operator, the has interception does not work for the for...in loop.

let stu1 = {name: 'Zhang San', score: 59};
let stu2 = {name: 'Li Si', score: 99};

let handler = {
  has(target, prop) {
    if (prop === 'score' && target[prop] < 60) {
      console.log(`${target.name} Fail,`);
      return false;
    }
    return prop in target;
  }
}

let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);

'score' in oproxy1
// Zhang San failed
// false

'score' in oproxy2
// true

for (let a in oproxy1) {
  console.log(oproxy1[a]);
}
// Zhang San
// 59

for (let b in oproxy2) {
  console.log(oproxy2[b]);
}
// Li Si
// 99

In the above code, the has interception only takes effect on the in operator, and does not take effect on the for...in loop. As a result, the properties that do not meet the requirements are not excluded by the for...in loop.

Constructor block - construct()

The construct method is used to intercept the new command. The following is the writing method of the intercepted object.

var handler = {
  construct (target, args, newTarget) {
    return new target(...args);
  }
};

The construct method can take two parameters.

  • Target: target object
  • args: parameter object of constructor
  • newTarget: the constructor used by the new command when creating an instance object (p in the following example)
var p = new Proxy(function () {}, {
  construct: function(target, args) {
    console.log('called: ' + args.join(', '));
    return { value: args[0] * 10 };
  }
});

(new p(1)).value
// "called: 1"
// 10

The object returned by the construct method must be an object, otherwise an error will be reported.

var p = new Proxy(function() {}, {
  construct: function(target, argumentsList) {
    return 1;
  }
});

new p() // Report errors
// Uncaught TypeError: 'construct' on proxy: trap returned non-object ('1')

Property delete block - deleteProperty()

The deleteProperty method is used to intercept the delete operation. If the method throws an error or returns false, the current property cannot be deleted by the delete command.

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property

In the above code, the deleteProperty method intercepts the delete operator, and an error will be reported when deleting the attribute with the first character of underscore.

Note that the un configurable property of the target object itself cannot be deleted by the deleteProperty method, otherwise an error will be reported.

Add property block - defineProperty()

The defineProperty method intercepts the Object.defineProperty operation.

var handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // Will not enter into force

In the above code, the defineProperty method returns false, causing the addition of new properties to always be invalid.

Note that if the target object is non extensible, defineProperty cannot add properties that do not exist on the target object, otherwise an error will be reported. In addition, if a property of the target object is not writable or configurable, the defineProperty method must not change these two settings.

Description object interception - getOwnPropertyDescriptor()

The getOwnPropertyDescriptor method intercepts Object.getOwnPropertyDescriptor(), and returns a property description object or undefined.

var handler = {
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }

In the above code, the handler.getOwnPropertyDescriptor method returns undefined for the property name whose first character is underscore.

Object prototype interception - getPrototypeOf()

The getPrototypeOf method is mainly used to intercept and get the object prototype. Specifically, block the following operations.

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

Here is an example.

var proto = {};
var p = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});
Object.getPrototypeOf(p) === proto // true

In the above code, the getPrototypeOf method intercepts Object.getPrototypeOf(), and returns the proto object.

Note that the return value of the getPrototypeOf method must be an object or null, otherwise an error is reported. In addition, if the target object is non extensible, the getPrototypeOf method must return the prototype object of the target object.

Property key name block - ownKeys()

The ownKeys method is used to block the read operation of the object's own properties. Specifically, intercept the following operations.

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in loop

The following is an example of intercepting Object.keys().

let target = {
  a: 1,
  b: 2,
  c: 3
};

let handler = {
  ownKeys(target) {
    return ['a'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// [ 'a' ]

The above code intercepts the Object.keys() operation on the target object, and only returns the a attribute of the A, b, and c attributes.

The following example intercepts the property name whose first character is underscore.

let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

Note that when using the Object.keys method, three types of attributes will be automatically filtered by the ownKeys method and will not be returned.

  • Properties that do not exist on the target object
  • Property name is Symbol value
  • An enumerable property
let target = {
  a: 1,
  b: 2,
  c: 3,
  [Symbol.for('secret')]: '4',
};

Object.defineProperty(target, 'key', {
  enumerable: false,
  configurable: true,
  writable: true,
  value: 'static'
});

let handler = {
  ownKeys(target) {
    return ['a', 'd', Symbol.for('secret'), 'key'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// ['a']

In the above code, in the ownKeys method, the non-existent attributes (d), Symbol value (Symbol.for('secret ') and non traversable attributes (key) are explicitly returned, and the results are automatically filtered out.

The ownKeys method can also intercept Object.getOwnPropertyNames().

var p = new Proxy({}, {
  ownKeys: function(target) {
    return ['a', 'b', 'c'];
  }
});

Object.getOwnPropertyNames(p)
// [ 'a', 'b', 'c' ]

The for...in loop is also intercepted by the ownKeys method.

const obj = { hello: 'world' };
const proxy = new Proxy(obj, {
  ownKeys: function () {
    return ['a', 'b'];
  }
});

for (let key in proxy) {
  console.log(key); // No output
}

In the above code, ownkeys specifies that only a and b attributes are returned. Since obj does not have these two attributes, the for...in loop will not have any output.

The array member returned by the ownKeys method can only be a string or Symbol value. If there are other types of values, or if the returned value is not an array at all, an error will be reported.

var obj = {};

var p = new Proxy(obj, {
  ownKeys: function(target) {
    return [123, true, undefined, null, {}, []];
  }
});

Object.getOwnPropertyNames(p)
// Uncaught TypeError: 123 is not a valid property name

In the above code, although the ownKeys method returns an array, each array member is not a string or Symbol value, so an error is reported.

If the target object itself contains a non configurable property, the property must be returned by the ownKeys method, otherwise an error is reported.

var obj = {};
Object.defineProperty(obj, 'a', {
  configurable: false,
  enumerable: true,
  value: 10 }
);

var p = new Proxy(obj, {
  ownKeys: function(target) {
    return ['b'];
  }
});

Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'

In the above code, the a property of obj object is not configurable. In this case, the array returned by the ownKeys method must contain a, otherwise an error will be reported.

In addition, if the target object is non extensible, the array returned by the ownKeys method must contain all the attributes of the original object, and cannot contain redundant attributes, otherwise an error is reported.

var obj = {
  a: 1
};

Object.preventExtensions(obj);

var p = new Proxy(obj, {
  ownKeys: function(target) {
    return ['a', 'b'];
  }
});

Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible

In the above code, the obj object is not extensible. In this case, the array returned by the ownKeys method contains the extra property b of the obj object, resulting in an error.

Change prototype intercept - setPrototypeOf()

The setPrototypeOf method is mainly used to intercept the Object.setPrototypeOf method.

Here is an example.

var handler = {
  setPrototypeOf (target, proto) {
    throw new Error('Changing the prototype is forbidden');
  }
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden

In the above code, as long as the prototype object of target is modified, an error will be reported.

Note that this method can only return Booleans, otherwise it will be automatically converted to Booleans. In addition, if the target object is non extensible, the setPrototypeOf method must not change the prototype of the target object.

Remove Proxy instance - Proxy.revocable()

The Proxy.revocable method returns a cancelable Proxy instance.

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

The Proxy.revocable method returns an object whose proxy attribute is a proxy instance and the revoke attribute is a function. You can cancel the proxy instance. In the above code, when the revoke function is executed and the proxy instance is accessed, an error will be thrown.

A usage scenario of Proxy.revocable is that the target object is not allowed to be accessed directly, but must be accessed through a proxy. Once the access is completed, the proxy right will be revoked and no access will be allowed again.

this problem

Although Proxy can access the target object, it is not the transparent Proxy of the target object, that is, without any interception, it can not guarantee the consistent behavior with the target object. The main reason is that in the case of Proxy proxy, this keyword inside the target object points to Proxy proxy.

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

In the above code, once proxy proxy proxy target.m, the internal this of the latter is to point to proxy instead of target.

The following is an example. Due to the change of this point, Proxy cannot Proxy the target object.

const _name = new WeakMap();

class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane');
jane.name // 'Jane'

const proxy = new Proxy(jane, {});
proxy.name // undefined

In the above code, the name attribute of the target object jane is actually saved on the external WeakMap object [name], which is distinguished by this key. Because this points to proxy when accessed through proxy.name, the value cannot be retrieved, so undefined is returned.

In addition, the internal properties of some native objects can only be obtained through the correct this, so Proxy cannot Proxy the properties of these native objects.

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

In the above code, the getDate method can only be obtained on the Date object instance. If this is not a Date object instance, an error will be reported. At this time, this binding to the original object can solve this problem.

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);

proxy.getDate() // 1
Published 108 original articles, won praise 9, visited 50000+
Private letter follow

Keywords: Attribute Programming

Added by eruiz1973 on Mon, 13 Jan 2020 13:41:31 +0200