In the latest release of Vue3.0, uvu abandoned object.defineproperty and added Proxy to implement data hijacking, so what is the difference between the two functions? This article takes a closer look at their usage and pros and cons, and you’ll understand why Vue chooses Proxy.

This article was first published in the public number [front-end one read], more exciting content please pay attention to the latest news of the public number.

I met defineProperty

First let’s look at a definition of object.defineProperty () from MDN:

The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

Its syntax is to pass in three arguments:

Object.defineProperty(obj, prop, descriptor)

The functions of the three parameters are as follows:

  • Obj: Object for which attributes are to be defined.
  • Prop: The name or Symbol of the property to be defined or modified.
  • Descriptor: Property descriptor to define or modify.

Let’s look at a simple use of this function; Since it can define new attributes on objects, we can use it to add new attributes to objects:

var user = {}
Object.defineProperty(user, 'name', {
  value: 'xyf'
})
console.log(user)
Copy the code

The value in the descriptor is the property value that needs to be defined or modified on the object (if the object has the property itself, the modification operation will be performed). In addition to strings, it can also be other JS data types (values, functions, etc.).

The property descriptor is an object, so there is more to it than value. It has the following properties:

The property name role The default value
configurable Of this property onlyconfigurableIf true, the descriptor of the property can be changed and the property can be deleted from the corresponding object. false
enumerable Of this property onlyenumerableTrue, the property appears in the object’s enumerated properties. false
writable Of this property onlyenumerableTrue to be changed by the assignment operator. false
value The value corresponding to this property undefined
get Property that is called when the property is accessed. undefined
set This function is called when the property value is modified. This method takes an argument and passes in the this object at the time of assignment. undefined

configurable

Let’s look at the use of each attribute; First, the different property is used to specify whether the property can be configured (changed or deleted).

  • Whether the property can be modified after the first setting
  • Whether attributes can be deleted

If the property is configured with additional: False, the property still exists after being deleted.

var user = {}

Object.defineProperty(user, 'name', {
  value: 'xyf'.configurable: false.writable: true.enumerable: true,})delete user.name
Copy the code

In strict mode, an error is thrown:

"use strict";
var user = {}

Object.defineProperty(user, 'name', {
  value: 'xyf'.configurable: false.writable: true.enumerable: true,})//TypeError: Cannot delete property 'name' of #<Object>
delete user.name
Copy the code

The control system is configured with different signals :false and cannot be modified again.

var user = {}

Object.defineProperty(user, 'name', {
  value: 'xyf'.configurable: false.writable: true.enumerable: true,})//TypeError: Cannot redefine property: name
Object.defineProperty(user, 'name', {
  value: 'new',})Copy the code

enumerable

Enumerable is used to describe whether a property can appear in a for in or object.keys () loop:

var user = {
    name: "xyf".age: 0};Object.defineProperty(user, "gender", {
    value: "m".enumerable: true.configurable: false.writable: false});Object.defineProperty(user, "birth", {
    value: "2020".enumerable: false.configurable: false.writable: false});for (let key in user) {
    console.log(key, "key");
}

console.log(Object.keys(user));
Copy the code

Obviously gender which enumerable is true is iterated, but birth is not.

writable

Writable specifies whether the value of an attribute can be overridden. If the value is false, the attribute can only be read:

var user = {};

Object.defineProperty(user, "name", {
    value: "xyf".writable: false.enumerable: false.configurable: false}); user.name ="new";
console.log(user);
Copy the code

Reassigning the name attribute in non-strict mode silently fails without throwing an error; In strict mode, an exception is thrown:

"use strict";
var user = {};

Object.defineProperty(user, "name", {
    value: "xyf".writable: false.enumerable: false.configurable: false});//TypeError: Cannot assign to read only property 'name' of object '#<Object>'
user.name = "new";
Copy the code

get/set

When you need to set or obtain properties of an object, you can use getter/setter methods:

var user = {};

var initName = ' '
Object.defineProperty(user, "name", {
    get: function(){
        console.log('get name')
        return initName
    },
    set: function(val){
        console.log('set name')
        initName = val
    }
});
// get name
console.log(user.name)
// set name
user.name = 'new'
Copy the code

The get and set functions are called once, respectively, when name is obtained and when name is assigned. Return user.name and user.name = val in the get and set functions.

If we return user.name directly from the get function, the user.name will also call get once, which will result in an infinite loop. The same is true for the set function, so we use a third-party variable, initName, to prevent an infinite loop.

But if we need to delegate more properties, it is not possible to define a third party variable for each property, which can be solved with closures

Note: The get and set functions do not have to occur in pairs. Both functions default to undefined if not set.

summary

The default value of any descriptor, enumerable and writable, is false. If you add a property to an Object using Object.defineProperty, the default value of any descriptor, enumerable and writable is different, and runs without any preset property. Then these values are false:

var user = {};

Object.defineProperty(user, "name", {
    value: "xyf"});/ / equivalent to the
Object.defineProperty(user, "name", {
    value: "xyf".configurable: false.enumerable: false.writable: false});Copy the code

When assigning attributes via the dot operator, all three descriptors are assigned true by default:

var user = {};

user.name = "xyf"

/ / equivalent to the

Object.defineProperty(user, "name", {
    value: "xyf".configurable: true.enumerable: true.writable: true});Copy the code

Attribute descriptor classification

Attribute descriptors mainly have two forms: data descriptors and access descriptors; Two properties specific to data descriptors: Value and writable; Access descriptors are specific to two properties: GET and set; The two types of attribute descriptors should not be mixed, otherwise an error will be reported. Here is an example of an error:

var user = {};

var initName = ' '

//TypeError: Invalid property descriptor. 
//Cannot both specify accessors and a value or writable attribute, #<Object>
Object.defineProperty(user, "name", {
    value: 'new'.writable: true.get: function(){
        console.log('get name')
        return initName
    },
    set: function(val){
        console.log('set name')
        initName = val
    }
});
Copy the code

It is easy to understand why the two descriptions should not be mixed; Value is used to define the value of an attribute, while GET and set are also used to define and modify the value of an attribute. The two descriptors have obvious similarities in function.

Although data descriptors and access descriptors cannot be used together, they can both be used with Configrable and Enumerable, respectively. The table below shows the keys that both descriptors can have:

configurable enumerable value writable get set
Data descriptor Yes Yes Yes Yes No No
Access descriptor Yes Yes No No Yes Yes

defects

We can see from the above code that although Object.defineProperty can hijack attributes of an Object, each attribute of the Object needs to be traversed. If the object has a new attribute, it needs to hijack the new attribute again. If the property is an object, you also need deep traversal. This is why Vue needs to pass $set to add attributes to an Object. The principle is also to hijack new attributes again via Object.defineProperty.

Object.defineproperty can hijack arrays as well as objects’ attributes; Although arrays have no properties, we can treat arrays’ indexes as properties:

var list = [1.2.3]

list.map((elem, index) = > {
    Object.defineProperty(list, index, {
        get: function () {
            console.log("get index:" + index);
            return elem;
        },
        set: function (val) {
            console.log("set index:"+ index); elem = val; }}); });// set index:2
list[2] = 6
// get index:1
console.log(list[1])
Copy the code

Although we are listening for changes to the elements in the array, we face the same problem as the listener properties: new elements do not trigger listener events:

var list = [1.2.3];

list.map((elem, index) = > {
    Object.defineProperty(list, index, {
        get: function () {
            console.log("get index:" + index);
            return elem;
        },
        set: function (val) {
            console.log("set index:"+ index); elem = val; }}); });// No output
list.push(4)
list[3] = 5
Copy the code

To do this, Vue’s solution is to hijack seven functions on the array.property prototype chain, which we do simply by using the following function:

const arratMethods = [
    "push"."pop"."shift"."unshift"."splice"."sort"."reverse",];const arrayProto = Object.create(Array.prototype);

arratMethods.forEach((method) = > {
    const origin = Array.prototype[method];
    arrayProto[method] = function () {
        console.log("run method", method);
        return origin.apply(this.arguments);
    };
});

const list = [];

list.__proto__ = arrayProto;

//run method push
list.push(2);
//run method shift
list.shift(3);
Copy the code

Classes, stereotypes, and inheritance in JAVASCRIPT

Instance objects can get properties and methods on prototype objects

The push, shift, and other functions we operate on arrays are functions on the called prototype object, so we rebind the rewritten prototype object to __proto__ on the instance object, which can be hijacked.

In addition, modifying the length property of the array directly will also cause listening on Object.defineProperty to fail:

var list = [];

list.length = 10;

list.map((elem, index) = > {
    Object.defineProperty(list, index, {
        get: function () {
            console.log("get index:" + index);
            return elem;
        },
        set: function (val) {
            console.log("set index:"+ index); elem = val; }}); }); list[5] = 4;
// undefined
console.log(list[6]);
Copy the code

By changing length to 10 there were 10 un______ in the array and although we hijacked each element we didn’t fire the get/set function.

Let’s summarize the pitfalls of Object.defineProperty when hijacking objects and arrays:

  1. Unable to detect addition or deletion of object attributes
  2. Unable to detect array element changes, array methods need to be overridden
  3. Unable to detect changes in array length

Proxy

In contrast to Object.defineProperty hijacking a property, Proxy is more thorough. Instead of limiting a property, Proxy directly proxies the entire Object. Let’s take a look at the ES6 documentation for Proxy:

Proxy can be understood as a layer of “interception” before the target object. All external access to the object must pass this layer of interception. Therefore, Proxy provides a mechanism for filtering and rewriting external access.

Let’s take a look at Proxy syntax:

var proxy = new Proxy(target, handler);
Copy the code

Proxy itself is a constructor. New Proxy generates intercepting instance objects for external access. The target in the constructor is the target object that we need to delegate, either an object or an array; Handler, like the Descriptor in Object.defineProperty, is an Object used to customize proxy rules.

var target = {}

var proxyObj = new Proxy(
    target,
    {
        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);
        },
        deleteProperty: function (target, propKey) {
            console.log(`delete ${propKey}! `);
            delete target[propKey];
            return true; }});//setting count!
proxyObj.count = 1;
//getting count!
/ / 1
console.log(proxyObj.count)
//delete count!
delete proxyObj.count
Copy the code

You can see that the Proxy directly represents the whole object of target, and returns a new object. It listens for changes in the properties of the Proxy object to obtain changes in the properties of the target object. We also found that the Proxy can listen not only for attribute additions but also for attribute deletions, much more powerful than Object.defineProperty.

In addition to objects, let’s see how the Proxy behaves with arrays:

var list = [1.2]
var proxyObj = new Proxy(list, {
    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}:${value}! `);
        return Reflect.set(target, propKey, value, receiver); }})//setting 1:3!
proxyObj[1] = 3
//getting push!
//getting length!
//setting 2:4!
//setting length:3!
proxyObj.push(4)
//setting length:5!
proxyObj.length = 5
Copy the code

Proxy can listen for changes in array subscript or array length, as well as function calls. In addition to commonly used GET and set, Proxy supports 13 interception operations.

You can see that Proxy has obvious syntax and functionality advantages over Object.defineProperty. Also, any defects in Object.defineProperty are well resolved by Proxy.

For more front-end information, please pay attention to the public number [front-end reading].

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles