ES 拾遺之賦值操做與原型鏈查找

問題

這兩天在排查一個 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 控制檯跑一下就會發現,報錯了😑
image.png
從報錯信息很容易判斷,咱們在嘗試給一個 readonly 的屬性作賦值,但關鍵是,prototype 這個屬性在 boundFn 上壓根不存在呀!
咱們知道,對象的屬性賦值操做的基本邏輯是這樣的:git

  1. 若是對象上該屬性不存在,則建立一個自有屬性並賦值
  2. 若是對象上該屬性已存在,則修改該屬性的值,修改過程會觸發該屬性上的 data descriptor(writable 配置)檢測或 accessor descriptor (setter 配置) 的調用。

毫無疑問上面代碼走的應該是第一個邏輯分支,徹底不該該報錯纔對。


起初我還覺得是瀏覽器兼容問題,而後嘗試過幾個瀏覽器以後,發現都是報錯😑


排查的過程當中發現,OfflineAudioContext.prototype 自己是 readonly 的
image.png
可是這跟咱們 boundFn.prototype 賦值有什麼關係呢,即使咱們把賦值操做改爲:github

boundFn.prototype = 123;

報錯仍是會照舊。
繼續查,發現 boundFn 的原型鏈上是有 prototype 的:
image.png
並且原型鏈上的這個 prototype 也是 readonly 的:image.png
可是咱們一個寫操做跟原型鏈有啥關係呢,不是讀操做時纔會按原型鏈查找嗎???
算法

ES Spec 追蹤

各類嘗試以後無果,這時候只能祭出 ecmascript spec,看看能不能從裏面找到蛛絲馬跡了😑


搜索找到賦值操做(assignment)相關的 spec 說明
image.png
若是有過讀 ecmascript spec 經驗的話,會找到關鍵步驟在第 5 步 PutValue
image.png
咱們這個場景裏,PutValue 的操做會沿着 4.a.false 的路徑執行。即 put 對應的調用爲 base.[[Put]](reference name, W, true)
找到 [[[Put]]](https://262.ecma-internationa... 的調用算法說明:
image.png
這裏其實就能看到,若是咱們走到了最後一步第6步的時候,實際上發生的事情就會是:
Object.defineProperty(O, P, { writable: true, enumerable: true, configurable: true, value: V }), 也就是咱們會爲對象建立一個新的屬性並賦值,且這個屬性是可枚舉可修改的,符合咱們以前的認知。


那其實咱們就要看看,爲何流程沒有走到第6步。
先看第一步裏的 [[[CanPut]]](https://262.ecma-internationa... 作了啥:
image.png
簡單翻譯下流程就是:chrome

  1. 查找自身屬性的 descriptor
  2. 若是有則按照 descriptor 的規則判斷
  3. 若是沒有則看對象是否有原型
  4. 若是原型是 null 則直接根據對象是否可拓展返回結果
  5. 不然去原型鏈上查找屬性
  6. 若是原型鏈上找不到,則直接根據對象是否可拓展返回結果
  7. 若是原型鏈上能找到,則記錄查找後的值對應的 descriptor
  8. 若是記錄的值是 accessor descriptor,那麼就根據 setter 配置決定返回值
  9. 若是記錄的值是 data descriptor,那麼就根據是否和拓展或者是否 writable 來給出返回值


其實到這裏咱們就能發現端倪了,關鍵點是這幾步:
image.png
這幾步描述的實際就是,計算流程會一直去原型鏈上查找屬性 P。


也就是說,即使咱們是賦值操做,只要是對象屬性的賦值,都會觸發原型鏈的查找。


那麼回到上面那段代碼,對應的計算流程就是:瀏覽器

  1. 先觸發了 boundFn 自身屬性裏查找 prototype 的操做
  2. 發現不存在 prototype,則去原型鏈上找
  3. 因爲 boundFn 的原型指向了 BaseAudioContext,因此返回的實際是 BaseAudioContext.prototype
  4. 而 BaseAudioContext.prototype 的 writable 配置爲 false
  5. 故 [[CanPut]] 操做返回了 false
  6. 返回 false 後就直接 throw 了一個 TypeError

解法

那麼若是咱們確實想給 boundFn 加一個自身屬性 prototype 該怎麼作呢?
其實咱們只要找到不會觸發原型鏈查找的修改方式就能夠了:ecmascript

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

原理就是 defineProperty API 不會有 [[getProperty]] 這種觸發原型鏈查找的調用:
image.png
函數

結論

賦值(assignment)操做也會存在原型鏈查找邏輯,且是否可寫也會遵循查找到的屬性的 descriptor 規則。spa

相關文章
相關標籤/搜索