这两天在排查一个 qiankun 的 bug 时,发现了一个我没法解释的 js 问题,这可要了个人命。
略去一切细枝末节,咱们直接先来看问题。
假若有这么一段代码:javascript
(() => { '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')); })();
假设咱们已知,函数经过 bind 调用后,返回的新的 boundFn 是必定不会有 prototype 的。
那么打印结果就应该是:java
false true
由于 boundFn 不具有自有属性 'prototype',因此在通过 boundFn.prototype = OfflineAudioContext.prototype
的赋值操做后,会为其建立一个新的自有属性 'prototype',其值为 OfflineAudioContext.prototype
。一切都在情理之中。
但你真的把这段代码粘到 chrome 控制台跑一下就会发现,报错了😑
从报错信息很容易判断,咱们在尝试给一个 readonly 的属性作赋值,但关键是,prototype 这个属性在 boundFn 上压根不存在呀!
咱们知道,对象的属性赋值操做的基本逻辑是这样的:git
毫无疑问上面代码走的应该是第一个逻辑分支,彻底不该该报错才对。
起初我还觉得是浏览器兼容问题,而后尝试过几个浏览器以后,发现都是报错😑
排查的过程当中发现,OfflineAudioContext.prototype 自己是 readonly 的
可是这跟咱们 boundFn.prototype 赋值有什么关系呢,即使咱们把赋值操做改为:github
boundFn.prototype = 123;
报错仍是会照旧。
继续查,发现 boundFn 的原型链上是有 prototype 的:
并且原型链上的这个 prototype 也是 readonly 的:
可是咱们一个写操做跟原型链有啥关系呢,不是读操做时才会按原型链查找吗???
算法
各类尝试以后无果,这时候只能祭出 ecmascript spec,看看能不能从里面找到蛛丝马迹了😑
搜索找到赋值操做(assignment)相关的 spec 说明:
若是有过读 ecmascript spec 经验的话,会找到关键步骤在第 5 步 PutValue:
咱们这个场景里,PutValue 的操做会沿着 4.a.false 的路径执行。即 put 对应的调用为 base.[[Put]](reference name, W, true)
。
找到 [[[Put]]](https://262.ecma-internationa... 的调用算法说明:
这里其实就能看到,若是咱们走到了最后一步第6步的时候,实际上发生的事情就会是:Object.defineProperty(O, P, { writable: true, enumerable: true, configurable: true, value: V })
, 也就是咱们会为对象建立一个新的属性并赋值,且这个属性是可枚举可修改的,符合咱们以前的认知。
那其实咱们就要看看,为何流程没有走到第6步。
先看第一步里的 [[[CanPut]]](https://262.ecma-internationa... 作了啥:
简单翻译下流程就是:chrome
其实到这里咱们就能发现端倪了,关键点是这几步:
这几步描述的实际就是,计算流程会一直去原型链上查找属性 P。
也就是说,即使咱们是赋值操做,只要是对象属性的赋值,都会触发原型链的查找。
那么回到上面那段代码,对应的计算流程就是:浏览器
那么若是咱们确实想给 boundFn 加一个自身属性 prototype 该怎么作呢?
其实咱们只要找到不会触发原型链查找的修改方式就能够了:ecmascript
- boundFn.prototype = OfflineAudioContext.prototype; + Object.defineProperty(boundFn, 'prototype', { value: OfflineAudioContext.prototype, enumerable: false, writable: true })
原理就是 defineProperty API 不会有 [[getProperty]] 这种触发原型链查找的调用:
函数
赋值(assignment)操做也会存在原型链查找逻辑,且是否可写也会遵循查找到的属性的 descriptor 规则。spa