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

相关文章
相关标签/搜索