preface

A few days ago when the interview, the interviewer asked this question, I feel I answer is not particularly good, here to tidy up ~ the article length will be longer, but after reading will harvest full ~ HOPE you insist on reading it ~

Object.defineProperty()

Function: Defines a new attribute on an object, or modifies an existing attribute on an object, and returns the object.

1. Basic use

Syntax: Object.defineProperty(obj, prop, Descriptor)

Parameters:

  1. The object to which attributes are to be added
  2. The name of the property to define or modify or [Symbol]
  3. The property descriptor to define or modify

Look at a simple example

let person = {}
let personName = 'lihua'
// Add the attribute name to the person object with the value personName
Object.defineProperty(person, 'name', {
  // The default is not enumerable, but: enumerable: true
  // The default value cannot be changed: wirtable: true
  // The default information cannot be deleted, and can be: 64x: true
    get: function () {
        return personName
    },
    set: function (val) {
        return name=val
    }
})
// When the name property of the Person object is read, the get method is fired
console.log(person.name)
// When the name property of the Person object is modified, the set method is fired
personName = 'liming'
// Check that the modification is successful
console.log(person.name)
Copy the code

In this way, we successfully listen for changes to the Name property on Person.

2. Listen for multiple properties on the object

In the above example, we only listen for one property change, but in the real world, we usually need to listen for multiple property changes at once. Here we need to iterate with Object.keys(obj). This method returns an array of characters from all the enumerable properties of an obj object. Here is a simple example of how this API works:

var obj = { 0: 'a'.1: 'b'.2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
Copy the code

Using this API, we can iterate over all the attributes of the hijacked object, but if we simply combine the above ideas with the API, we will find that the effect is not achieved. Here is an incorrect version of this:

Object.keys(person).forEach(function (key) {
    Object.defineProperty(person, key, {
        enumerable: true.configurable: true.// This is passed by default
        get() {
            return person[key]
        },
        set(val) {
            console.log('to person${key}Property modified ')
            person[key] = val
            // Render operation can be performed after modification}})})console.log(person.age)
Copy the code

It doesn’t look like there’s anything wrong with the code above, but give it a try and you’ll overflow the stack just like I did. Why is that? Let’s focus on the get method. When we access a property on a Person, we fire the GET method, which returns the Person [key], but accessing the person[key] also fires the GET method, causing a recursive call and eventually stack overflow. This leads us to the following method, we need to set a transit Obsever so that the return value in get does not directly access obj[key].

let person = {
    name: ' '.age: 0
}
// Implement a reactive function
function defineProperty(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(Visited `${key}Attribute `)
            return val
        },
        set(newVal) {
            console.log(`${key}Property is modified to${newVal}The `)
            val = newVal
        }
    })
}
// Implement a traversal function Observer
function Observer(obj) {
    Object.keys(obj).forEach((key) = > {
        defineProperty(obj, key, obj[key])
    })
}
Observer(person)
console.log(person.age)
person.age = 18
console.log(person.age)
Copy the code

3. Listen deeply to an object

So how do we solve for nested pairs of objects? In fact, on the basis of the above code, and a recursive, can easily achieve ~ we can observe that actually Obsever is that we want to realize the surveillance function, we expected goal is: as long as the object to which the attributes of the object can be achieved for the surveillance, even if the object’s properties and an object. Let’s add a recursive case to defineProperty() :

function defineProperty(obj, key, val) {
    // If the property of an object is also an object, recurse into the object and listen
    observer(key)
    Object.defineProperty(obj, key, {
        get() {
            console.log(Visited `${key}Attribute `)
            return val
        },
        set(newVal) {
            console.log(`${key}Property is modified to${newVal}The `)
            val = newVal
        }
    })
}
Copy the code

Of course, we also need to add a recursive stop condition to the observer:

function Observer(obj) {
    // If an object is not passed in, return
    if (typeofobj ! = ="object" || obj === null) {
        return
    }
    // for (key in obj) {
    Object.keys(obj).forEach((key) = > {
        defineProperty(obj, key, obj[key])
    })
    // }

}
Copy the code

So that’s pretty much it, but there’s a little bit of a problem, if we modify a property, if the original property value is a string, but we reassign an object, how do we listen for all the properties of the newly added object? It’s as simple as modifying the set function:

set(newVal) {
    // If newVal is an object, recursively listen into the object
    observer(newVal)
    console.log(`${key}Property is modified to${newVal}The `)
    val = newVal
        }
Copy the code

So here we are

4. Listen on arrays

What if the object’s property is an array? How do we implement listening? Take a look at the following code:

let arr = [1.2.3]
let obj = {}
// listen on arR as an obj property
Object.defineProperty(obj, 'arr', {
    get() {
        console.log('get arr')
        return arr
    },
    set(newVal) {
        console.log('set', newVal)
        arr = newVal
    }
})
console.log(obj.arr)Get arr [1,2,3] normal
obj.arr = [1.2.3.4] // set [1,2,3,4] is normal
obj.arr.push(3) // Get arr is abnormal and push cannot be heard
Copy the code

We find that the set method does not listen to elements added to the array by the push method.

In fact, it is possible to access or modify existing elements in the array by index, but for elements added by push or unshift, an index is added. In this case, manual initialization is required so that the newly added elements can be heard. In addition, removing an element by pop or Shift removes and updates the index, as well as triggering setter and getter methods.

Proxy

Does it feel a bit complicated? In fact, we have a problem with adding a new property to an object that we need to manually listen for.

For this reason, when using Vue to add new attributes to arrays or objects in Data, you need to use vm.$set to ensure that the new attributes are also responsive.

As you can see, listening for data through Object.defineporperty () is cumbersome and requires a lot of manual handling. This is why Yu Creek switched to Proxy in Vue3.0. Let’s take a look at how Proxy solves these problems

1. Basic use

Const p = new Proxy(target, handler)

  1. Target: to be usedProxyWrapped target object (can be any type of object, including a native array, a function, or even another proxy)
  2. Handler: An object that usually has functions as properties, and the functions in each property define the agents that perform the various operationspBehavior.

Through Proxy, we can intercept some operations on the object where the Proxy is set. All external operations on this object must pass this layer of interception first. (Similar to defineProperty)

Let’s start with a simple example

// Define a proxy object
let person = {
    age: 0.school: 'China'
}
// Define the handler object
let hander = {
    get(obj, key) {
        // If the object has this attribute, return the value of the attribute, if not, return the default 66
        return key in obj ? obj[key] : 66
    },
    set(obj, key, val) {
        obj[key] = val
        return true}}// Pass the handler object to the Proxy
let proxyObj = new Proxy(person, hander)

// Test whether get intercepts successfully
console.log(proxyObj.age)//输出0
console.log(proxyObj.school)// Output west power
console.log(proxyObj.name)// Output the default value 66

// Test whether the set intercepts successfully
proxyObj.age = 18
console.log(proxyObj.age)18 Successfully modified
Copy the code

As you can see, the Proxy represents the entire object, not a specific attribute of the object, and does not require us to walk through the data binding one by one.

It is worth noting that after we added a property to the Object using Object.defineProperty(), we still read and write the property on the Object itself. But once we use Proxy, we need to operate on Proxy instance object proxyObj if we want the read and write operation to take effect.

In addition, MDN explicitly states that the set() method should return a Boolean value, or TypeError will be reported.

2. Easily resolve problems encountered in Object.defineProperty

When using Object.defineProperty above, we encountered the following problems:

1. You can only listen for one attribute at a time. You need to listen for all attributes. We solved that up here. 2. Recursive listening is required in cases where an object’s property is still an object. 4. For array elements added by push and unshift methods, it cannot be listened

These problems are easily solved in Proxy, so let’s look at the following code.

Check the second question

Building on the code above, let’s make the structure of the object a little more complex.

let person = {
    age: 0.school: 'China'.children: {
        name: 'Ming'}}let hander = {
    get(obj, key) {
        return key in obj ? obj[key] : 66
    }, set(obj, key, val) {
        obj[key] = val
        return true}}let proxyObj = new Proxy(person, hander)

The get / / test
console.log(proxyObj.children.name)// Output: xiao Ming
console.log(proxyObj.children.height)// output: undefined
/ / test set
proxyObj.children.name = 'food dish'
console.log(proxyObj.children.name)// Output
Copy the code

Height = ‘children’; height = ‘children’;

Check the third question

Proxyobj. name is a property that does not exist in the original object, but can still be intercepted by get when accessing it.

Check the fourth question

let subject = ['advanced mathematics']
let handler = {
    get(obj, key) {
        return key in obj ? obj[key] : 'There is no such subject'
    }, set(obj, key, val) {
        obj[key] = val
        // The set method should return true on success, otherwise an error is reported
        return true}}let proxyObj = new Proxy(subject, handler)

// check get and set
console.log(proxyObj)// output [' high number ']
console.log(proxyObj[1])// There is no such discipline for output
proxyObj[0] = 'University Physics'
console.log(proxyObj)// output [' university physics']

// // verifies that the element added by push can be listened on
proxyObj.push('Linear algebra')
console.log(proxyObj)// output [' college physics ', 'Linear Algebra']
Copy the code

So this is the perfect solution to our previous problem.

3.Proxy supports 13 interception operations

In addition to get and set to intercept read and assignment operations, Proxy supports interception of a variety of other behaviors. Here is a brief introduction. If you want to know more about it, go to MDN.

  • Get (target, propKey, receiver) : Intercepts 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 propKey in proxy operation and returns a Boolean value.

  • DeleteProperty (target, propKey) : Intercepts the operation of delete Proxy [propKey] and returns a Boolean value.

  • OwnKeys (target) : interception Object. GetOwnPropertyNames (proxy), Object. GetOwnPropertySymbols (proxy), the Object. The keys (proxy), for… The in loop returns an array. This method returns the property names of all of the target Object’s own properties, whereas object.keys () returns only the traversable properties of the target Object itself.

  • GetOwnPropertyDescriptor (target, propKey) : interception Object. GetOwnPropertyDescriptor (proxy, propKey), returns the attributes describe objects.

  • 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), returns a Boolean.

  • GetPrototypeOf (target) : Intercepts object.getProtoTypeof (proxy) and returns an Object.

  • IsExtensible (Target) : Intercepts Object. IsExtensible (proxy), returning 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) : intercepts operations called by Proxy instances as functions, such as Proxy (… The args), proxy. Call (object,… The args), proxy. Apply (…). .

  • Construct (target, args) : intercepts operations called by Proxy instances as constructors, such as new Proxy (… The args).

4. Questions about this in Proxy

Although the Proxy does the work of proxying the target object, it is not a transparent Proxy, that is, even if the handler is an empty object (that is, does not do any proxying), the “this” in the object it is proxyObj points to is not that object, but a proxyObj object. Let’s look at an example:

let target = {
    m() {
        // check if this points to proxyObkj
        console.log(this === proxyObj)
    }
}
let handler = {}
let proxyObj = new Proxy(target, handler)

proxyObj.m()/ / output: true
target.m()/ / output: false
Copy the code

As you can see, this inside the proxied object target points to proxyObj. This orientation can sometimes lead to problems. Let’s take a look at an example:

const _name = new WeakMap(a);class Person {
   // Store the person name on the name attribute of the _name
  constructor(name) {
    _name.set(this, name);
  }
  // When obtaining the name attribute of person, return the name of the _name
  get name() {
    return _name.get(this); }}const jane = new Person('Jane');
jane.name // 'Jane'

const proxyObj = new Proxy(jane, {});
proxyObj.name // undefined
Copy the code

In the above example, the name attribute of the Jane object is obtained by pointing to this, which in turn points to proxyObj, causing the proxy to fail.

In addition, some internal properties of js built-in objects can only be obtained by using the correct this, so Proxy cannot Proxy the properties of these native objects. Here’s an example:

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

proxyObj.getDate();
// TypeError: this is not a Date object.
Copy the code

If this is not an instance of a Date object, an error will be reported. If this is not an instance of a Date object, an error will be reported. So how do we solve this problem? Just manually bind this to an instance of a Date object, as shown in the following example:

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

End and spend

At this point, my summary is over ~ the article is not very comprehensive, there are many places not mentioned, such as:

  1. Proxy is often used withReflectuse

  2. We oftenObject.create()The Proxy () method adds the Proxy instance Object to the prototype Object of Object so that we can direct Object.proxyobj

  3. If you’re interested, try adding output to Proxy get and set. You’ll notice that when we call push,Get and set are printed twice eachWhy is that?

There is no end to learning. Let’s work together

You are welcome to correct any mistakes

Reference article:

1.Proxy and Object.defineProperty are introduced and compared

2.MDN Proxy