The introduction

The reason for this is that while reading the Koa code recently, I found the “proxy” for CTX properties in Koa interesting.

Just in time to verify this, it turned out that Chrome had some long-standing bugs with this approach.

So I wrote this article to record the drip and hope to help more front-end students avoid stepping on pits.

This article focuses on the “masking” function of Getter/Setter property access/operators in JavaScript.

I’ll spend a little more time describing how it looks and works, but trust me. This is a technical popular science article, not the so-called clever non-technical cool article.

Basic object operations

First let’s look at a very common JS code:

const parent = {
  name: '19Qingfeng'
}

const child = Object.create(parent)

child.name = 'WangHaoyu'
console.log(child)
console.log(child.name)
Copy the code

Create a new Object, child, with the protot of the child pointing to the parent Object.

Let’s look at the print:

When we execute child.name = ‘WangHaoyu’, we are essentially manipulating the assignment on the Child instance object and naturally have nothing to do with the name property on the prototype.

Also, when we access child.name, we won’t look it up in the prototype chain because the instance itself has the name attribute. WangHaoyu should be exported.

See some friends may have impatience, feel how simple the basis why to take out the waste of everyone’s time.

Don’t worry. Let’s move on. This is just the groundwork before we start.

Getter/Setter

When you define an object in JavaScript, you can use [[Getter]] and [[Setter]] to bind the corresponding execution function for the property.

In a nutshell, like this:

const obj = {
  _name: null.get name() {
    return this._name
  },
  set name(value) {
    this._name = value
  }
}

obj.name = '19Qingfeng'

console.log(obj.name) // 19Qingfeng
Copy the code

For example, in the example above we define a get accessor name for the obj object, and we also define the corresponding setter property manipulation method for name.

When obj.name = ’19Qingfeng’ is executed, it actually calls the setter function named name on obj to modify the _name value on the obj instance object.

Similarly, when console.log(obj.name) is called, an LTS query is performed (of course, the assignment above triggers not only LTS but also RHS).

Accessing the name property on obj triggers the corresponding get function execution to obtain the _name property on obj and print 19Qingfeng.

In fact, the above code is equivalent to:


const obj = {
  _name: null
}

Object.defineProperty(obj, 'name', {
  get() {
    return this._name
  },
  set(value) {
    this._name = value
  }
})


obj.name = '19Qingfeng'

console.log(obj.name) // 19Qingfeng
Copy the code

Again, I’m sure the basics are easy for everyone to follow. But when we combine inheritance with Getters/ setters, something unexpected happens.

Shielding effect

As for the so-called shielding effect, let’s take a look at a small example:

// Create parent object with get/set and _name
const parent = {
  _name: null.get name() {
    return this._name
  },
  set name(value) {
    this._name = value
  }
}

// Create an empty object child equivalent to child.__proto__ = parent
const child = Object.create(parent)

// Assign the name attribute to the child instance
child.name = '19Qingfeng'

console.log(child, 'child')
Copy the code

And if you think about it a little bit, it’s not that hard. Create creates an empty Object that inherits from the parent Object.

Later we try to add a name attribute of 19Qingfeng to the child instance with child.name = ’19Qingfeng’.

In our JavaScript sense, we add a name attribute to the child instance, which essentially has nothing to do with the parent operator in the prototype.

I was naive to think that when we assigned child.name = ’19Qingfeng’, we should have simply added a property named 19Qingfeng to the child instance.

But that’s not the case. Let’s look at the print:

If you are not familiar with the so-called “shielding effect”, the results will come as a surprise to you.

We assign to the instance child, but we have a 19Qinfeng named _name instead of a name attribute.

This is the masking effect of the so-called Getter/Setter that I want to highlight with you:

For example, when we assign the name attribute to the child, the complete process is as follows:

  • If the Child object contains a common data access attribute named name, the assignment statement modifies the value of the existing attribute.

  • If the name attribute does not exist on the Child instance object, then the child’s Prototype is iterated for the attribute.

  • If the name attribute is not found in the stereotype chain, then the name attribute is simply added directly to the Child instance object.

However, if the name attribute is queried in the child’s prototype chain, the situation becomes slightly more complicated.

  • The first is the familiar, if the attribute name appears in both the instance and prototype, then masking occurs in the first place.

Changes to the name attribute in child will mask all changes to the name attribute in the prototype, as we understand it.

Of course, if masking were really that easy, then I wouldn’t have to tell you about it at all, if there wasn’t a name attribute in the instance but there was a name on the prototype

  • In the first case, if a common data access attribute named name exists on child’s prototype chain and the attribute is not marked as writable (writable:false), then a name attribute will be added to the child instance. It will mask the attributes on the stereotype.

  • If a common data access attribute named name exists on child’s prototype chain and is marked as writable (writable:true), So child.name = ’19Qingfeng’ has no effect, in other words it does not add attributes to its own instance or modify attributes of the same name on the prototype, and in strict mode even this will cause an error.

  • And finally, in the third case, it explains what we just did. If child has a name in the stereotype chain and it’s a setter, then when we assign on the instance, the setter on the stereotype will be called, and the name property will not be added to the instance, It also doesn’t have any effect on the setter on the prototype.

Let’s explain the example in combination with the third case. First, there is no name property in the Child object itself, but it inherits from the parent object. There are so-called getters and setters called name on child’s prototype.

When we call child.name = ’19QIngfeng’, the third case of masking is satisfied:

Name = ’19QIngfeng’ on the instance does not add attributes to the instance and calls the most recent setter on the prototype, which is equivalent to executing this._name = value.

Since we call it with child.name, the name in the setter points to the corresponding child, and the logic in the setter simply adds a normal attribute for the child instance with a _name value of 19QIngfeng.

In fact, at this point, THE main point I want to explain to you is complete. So called getters/setters, there are some situations where property masking happens, and I think you’re pretty well aware of where that happens.

Chrome Bug

Finally, let’s take a look at Chrome’s rendering of this situation. Of course, you might also notice that all of the consoles we’ve been using have come from FireFox, which I did intentionally.

For a piece of code like this, I’m sure you already know how it works:

const parent = {
  _name: null.get name() {
    return this._name
  },
  set name(value) {
    this._name = value
  }
}

const child = Object.create(parent)


child.name = '19Qingfeng'

console.log(child)
Copy the code

If you’re still confused about the code at this point, I encourage you to go back and re-understand the implementation described earlier in this article.

Let’s take a look at Console in Google Chrome:

It is obvious that there should be no name attribute at all on the Child instance object, only the corresponding _NAME attribute.

This is probably a minor problem in Chrome, if you choose to use Chrome result printing to understand the effect of Getter/Setter property masking, then I believe you will never be able to get out of it.

But all of that is not so important anymore, what is important is the conclusion that I have already told you.

When we assign to an object, we don’t just assign to the instance directly. Different situations have different effects.

In terms of what it can do for us, those of you who know ES6 can remember a little bit about Proxy in ES6, which is Vue 3’s data hijacking for Reactive implementations.

It is entirely possible to implement polyfill schemes similar to Get and Set traps in ES6 proxies using the “masking” effects of getters/setters mentioned above.

If a Getter/Setter of the same name exists on the prototype and the instance itself does not exist, then the Getter/Setter on the nearest prototype will be triggered to mask the operation on the instance.

Actually in Koa is using this effect to achieve simple hijacked, similar to the Proxy specific code, everyone can be in Koa/lib/application. The createContext/js as seen in the constructor.

At the end

Finally, we come to the end of the article. First of all, at the end of the article, THANK every student who can read here.

The knowledge point emphasized in this article does not exist what great difficulty, even can be said to be a very small knowledge point.

But after all, “to a thousand miles” must have been a small step, right?