The problem

These days, I found a JS problem that I could not explain when I was investigating a bug of Qiankun, which cost me my life. Let’s cut out all the details and come to the problem first. Suppose you have code like this:

(() = > {
  'use strict';
  
  const boundFn = Function.prototype.bind.call(OfflineAudioContext, window);
  console.log(boundFn.hasOwnProperty(boundFn, 'prototype'));
  boundFn.prototype = OfflineAudioContext.prototype;
  console.log(boundFn.hasOwnProperty(boundFn, 'prototype')); }) ();Copy the code

Assuming we know that the new boundFn returned by the bind call is bound to have no prototype. The printed result should be:

false
true
Copy the code

Because boundFn doesn’t have its own property ‘Prototype’, it is passedboundFn.prototype = OfflineAudioContext.prototypeAfter the assignment operation, a new property of its own, ‘Prototype’, is created for itOfflineAudioContext.prototype. It all makes sense.



But if you really paste this code into chrome console and run it, you will find that the error 😑



It’s easy to tell from the error message that we’re trying to assign a readOnly property, but the key is that the Prototype property doesn’t exist on boundFn!

As we know, the basic logic of an object’s attribute assignment operation is as follows:

  1. If the property does not exist on the object, a property of its own is created and assigned
  2. If the property already exists on the object, modify the value of the property. The modification process triggers the call of the data Descriptor (Writable configuration) detection or accessor Descriptor (setter configuration) on the property.

There is no doubt that the above code should follow the first logical branch, and should not report an error at all.



At first I thought it was a browser compatibility problem, but after trying several browsers, I found that they all reported an error 😑



Found in the process of screening, OfflineAudioContext prototype itself is readonly



But what does this have to do with our boundFn. Prototype assignment, even if we change it to:

boundFn.prototype = 123;
Copy the code

The error will still be the same.

BoundFn has a prototype on its chain:



And the prototype on the chain is readonly:

But we have a write operation with the prototype chain what relationship, not read operation will be according to the prototype chain search??

ES Spec tracking

After various attempts with no result, we can only sacrifice ecMAScript Spec to see if we can find any clues from it 😑



Search to find assignment operations (Assignment) relatedThe spec shows:



If you’ve ever read the EcMAScript spec, you’ll find the key step in step 5PutValue:



In our scenario, the PutValue operation is executed along the 4.a.price path. The call corresponding to put isbase.[[Put]](reference name, W, true).

find[[Put]]Call algorithm description:



And you can actually see here, if we get to the last step, step 6, what’s actually happening is this:

Object.defineProperty(O, P, { writable: true, enumerable: true, configurable: true, value: V }), that is, we will create a new property for the object and assign a value, and this property is enumerable and modifiable, according to our previous knowledge.



So let’s actually look at why the process doesn’t go to step 6.

Let’s look at step 1[[CanPut]]What do the:



The process is as follows:

  1. Find its own property descriptor
  2. If so, judge according to the rules of Descriptor
  3. If not, see if the object has a prototype
  4. If the stereotype is null, the result is returned directly based on whether the object is extensible
  5. Otherwise, look up the property on the prototype chain
  6. If the prototype is not found on the chain, the result is returned directly based on whether the object is extensible
  7. If it can be found on the prototype chain, record the descriptor of the searched value
  8. If the value of the record is accessor Descriptor, the return value is determined according to the setter configuration
  9. If the value of the record is a data descriptor, then the return value is given based on whether or not the record is extended or writable


In fact, we can find the clues here, the key is the steps:



What these steps really describe is that the computation process will go all the way up the prototype chain looking for property P.



In other words, even if we’re doing an assignment,Any assignment to an object property triggers a prototype chain lookup.



So back to the above code, the corresponding calculation flow is:

  1. The prototype lookup on boundFn itself is triggered first
  2. If no prototype is found, go to the prototype chain
  3. Pointed to the BaseAudioContext boundFn prototype, so return the actual is BaseAudioContext prototype
  4. While BaseAudioContext. Prototype writable configuration to false
  5. So the [[CanPut]] operation returns false
  6. Return false and throw a TypeError directly

solution

So what if we do want to add a prototype property to boundFn? We just need to find a modification that doesn’t trigger a prototype chain lookup:

- boundFn.prototype = OfflineAudioContext.prototype;
+ Object.defineProperty(boundFn, 'prototype', { value: OfflineAudioContext.prototype, enumerable: false, writable: true })
Copy the code

The principle isdefineProperty APIThere will be no [[getProperty]] call that triggers a prototype chain lookup:



conclusion

Assignment operations also have prototype chain lookup logic, and writability follows the descriptor rules of the attributes found.