這兩天在排查一個 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